diff --git a/examples/alb/index.ts b/examples/alb/index.ts index 78d95c62..22af2cbe 100644 --- a/examples/alb/index.ts +++ b/examples/alb/index.ts @@ -3,24 +3,23 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as pulumi from '@pulumi/pulumi'; import * as pulumicdk from '@pulumi/cdk'; +import { CfnOutput } from 'aws-cdk-lib'; class AlbStack extends pulumicdk.Stack { url: pulumi.Output; - constructor(id: string, options?: pulumicdk.StackOptions) { - super(id, options); - // necessary for local testing - const t = this as any; + constructor(app: pulumicdk.App, id: string) { + super(app, id); - const vpc = new ec2.Vpc(t, 'VPC'); + const vpc = new ec2.Vpc(this, 'VPC'); - const asg = new autoscaling.AutoScalingGroup(t, 'ASG', { + const asg = new autoscaling.AutoScalingGroup(this, 'ASG', { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), }); - const lb = new elbv2.ApplicationLoadBalancer(t, 'LB', { + const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { vpc, internetFacing: true, }); @@ -45,10 +44,18 @@ class AlbStack extends pulumicdk.Stack { }); this.url = this.asOutput(lb.loadBalancerDnsName); + } +} - this.synth(); +class MyApp extends pulumicdk.App { + constructor() { + super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => { + const stack = new AlbStack(scope, 'teststack'); + return { url: stack.url }; + }); } } -const stack = new AlbStack('teststack'); -export const url = stack.url; +const app = new MyApp(); + +export const url = app.outputs['url']; diff --git a/examples/api-websocket-lambda-dynamodb/index.ts b/examples/api-websocket-lambda-dynamodb/index.ts index 3ee11bff..3eabb1ac 100644 --- a/examples/api-websocket-lambda-dynamodb/index.ts +++ b/examples/api-websocket-lambda-dynamodb/index.ts @@ -10,8 +10,8 @@ import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; class ChatAppStack extends pulumicdk.Stack { public readonly url: Output; public readonly table: Output; - constructor(id: string) { - super(id); + constructor(app: pulumicdk.App, id: string) { + super(app, id); // initialise api const name = id + '-api'; @@ -93,11 +93,21 @@ class ChatAppStack extends pulumicdk.Stack { this.table = this.asOutput(table.tableName); this.url = this.asOutput(stage.url); + } +} - this.synth(); +class MyApp extends pulumicdk.App { + constructor() { + super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => { + const stack = new ChatAppStack(scope, 'chat-app'); + return { + url: stack.url, + table: stack.table, + }; + }); } } -const stack = new ChatAppStack('chat-app'); -export const url = stack.url; -export const table = stack.table; +const app = new MyApp(); +export const url = app.outputs['url']; +export const table = app.outputs['table']; diff --git a/examples/apprunner/index.ts b/examples/apprunner/index.ts index 2777ca17..958aafab 100644 --- a/examples/apprunner/index.ts +++ b/examples/apprunner/index.ts @@ -1,15 +1,12 @@ -import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as pulumi from '@pulumi/pulumi'; import * as pulumicdk from '@pulumi/cdk'; -import { Construct } from 'constructs'; import { Service, Source } from '@aws-cdk/aws-apprunner-alpha'; -import { CfnOutput } from 'aws-cdk-lib'; class AppRunnerStack extends pulumicdk.Stack { url: pulumi.Output; - constructor(id: string) { - super(id); + constructor(app: pulumicdk.App, id: string) { + super(app, id); const service = new Service(this, 'service', { source: Source.fromEcrPublic({ @@ -19,10 +16,17 @@ class AppRunnerStack extends pulumicdk.Stack { }); this.url = this.asOutput(service.serviceUrl); + } +} - this.synth(); +class MyApp extends pulumicdk.App { + constructor() { + super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => { + const stack = new AppRunnerStack(scope, 'teststack'); + return { url: stack.url }; + }); } } -const stack = new AppRunnerStack('teststack'); -export const url = stack.url; +const app = new MyApp(); +export const url = app.outputs['url']; diff --git a/examples/appsvc/index.ts b/examples/appsvc/index.ts index 58bd181d..f915bb09 100644 --- a/examples/appsvc/index.ts +++ b/examples/appsvc/index.ts @@ -22,8 +22,8 @@ const azs = aws.getAvailabilityZonesOutput({ class ClusterStack extends pulumicdk.Stack { serviceName: pulumi.Output; - constructor(name: string) { - super(name); + constructor(app: pulumicdk.App, name: string) { + super(app, name); const vpc = ec2.Vpc.fromVpcAttributes(this, 'Vpc', { vpcId: pulumicdk.asString(defaultVpc.id), @@ -82,11 +82,18 @@ class ClusterStack extends pulumicdk.Stack { ], }); - this.synth(); - this.serviceName = this.asOutput(service.serviceName); } } -const stack = new ClusterStack('teststack'); -export const serviceName = stack.serviceName; +class MyApp extends pulumicdk.App { + constructor() { + super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => { + const stack = new ClusterStack(scope, 'teststack'); + return { serviceName: stack.serviceName }; + }); + } +} + +const app = new MyApp(); +export const serviceName = app.outputs['serviceName']; diff --git a/examples/cron-lambda/index.ts b/examples/cron-lambda/index.ts index 644fe821..1f16c520 100644 --- a/examples/cron-lambda/index.ts +++ b/examples/cron-lambda/index.ts @@ -9,8 +9,8 @@ import * as pulumicdk from '@pulumi/cdk'; class LambdaStack extends pulumicdk.Stack { lambdaArn: pulumi.Output; - constructor(id: string, options?: pulumicdk.StackOptions) { - super(id, options); + constructor(app: pulumicdk.App, id: string) { + super(app, id); // Use the AWS CDK Lambda Function API directly. const lambdaFn = new aws_lambda.Function(this, 'lambda', { @@ -31,10 +31,17 @@ class LambdaStack extends pulumicdk.Stack { // Export the Lambda function's ARN as an output. this.lambdaArn = this.asOutput(lambdaFn.functionArn); + } +} - this.synth(); +class MyApp extends pulumicdk.App { + constructor() { + super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => { + const stack = new LambdaStack(scope, 'teststack'); + return { lambdaArn: stack.lambdaArn }; + }); } } -const stack = new LambdaStack('teststack'); -export const lambdaArn = stack.lambdaArn; +const app = new MyApp(); +export const lambdaArn = app.outputs['lambdaArn']; diff --git a/examples/ec2-instance/index.ts b/examples/ec2-instance/index.ts index 613c2ebe..08274a42 100644 --- a/examples/ec2-instance/index.ts +++ b/examples/ec2-instance/index.ts @@ -6,8 +6,8 @@ import * as pulumicdk from '@pulumi/cdk'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; export class Ec2CdkStack extends pulumicdk.Stack { - constructor(id: string) { - super(id); + constructor(app: pulumicdk.App, id: string) { + super(app, id); // Create a Key Pair to be used with this EC2 Instance // Temporarily disabled since `cdk-ec2-key-pair` is not yet CDK v2 compatible @@ -80,12 +80,19 @@ export class Ec2CdkStack extends pulumicdk.Stack { new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp, }); + } +} - this.synth(); +class MyApp extends pulumicdk.App { + constructor() { + super('app', (scope: pulumicdk.App) => { + new Ec2CdkStack(scope, 'teststack'); + }); } } -const stack = new Ec2CdkStack('teststack'); -export const ipAddress = stack.outputs['IP Address']; -export const keyCommand = stack.outputs['Download Key Command']; -export const sshCommand = stack.outputs['sshCommand']; +const app = new MyApp(); + +export const ipAddress = app.outputs['IP Address']; +export const keyCommand = app.outputs['Download Key Command']; +export const sshCommand = app.outputs['sshCommand']; diff --git a/examples/ecscluster/Pulumi.yaml b/examples/ecscluster/Pulumi.yaml deleted file mode 100644 index 83e1803e..00000000 --- a/examples/ecscluster/Pulumi.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: pulumi-cdk-ecscluster -runtime: nodejs -description: ECS Cluster diff --git a/examples/ecscluster/index.ts b/examples/ecscluster/index.ts deleted file mode 100644 index fdc97b69..00000000 --- a/examples/ecscluster/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as pulumi from '@pulumi/pulumi'; -import * as pulumicdk from '@pulumi/cdk'; -import * as ec2 from 'aws-cdk-lib/aws-ec2'; -import * as pulumiaws from "@pulumi/aws-native"; - -class ECSClusterStack extends pulumicdk.Stack { - clusterArn: pulumi.Output; - - constructor(id: string, options?: pulumicdk.StackOptions) { - super(id, options); - - const vpc = ec2.Vpc.fromLookup(this, 'MyVpc', { - isDefault: true, - }) - const cluster = new ecs.Cluster(this, 'fargate-service-autoscaling', { vpc }); - - this.clusterArn = this.asOutput(cluster.clusterArn); - - this.synth(); - } -} - -export const clusterArn = pulumiaws.getAccountId().then(account => { - const stack = new ECSClusterStack('teststack', { - props: { - env: { - region: pulumiaws.config.region, - account: account.accountId, - } - } - }); - return stack.clusterArn; -}); diff --git a/examples/ecscluster/tsconfig.json b/examples/ecscluster/tsconfig.json deleted file mode 100644 index c7c2de61..00000000 --- a/examples/ecscluster/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "outDir": "bin", - "target": "es2016", - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "experimentalDecorators": true, - "pretty": true, - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "forceConsistentCasingInFileNames": true - }, - "files": [ - "index.ts" - ] -} diff --git a/examples/examples_nodejs_test.go b/examples/examples_nodejs_test.go index 7d04de58..61dfc288 100644 --- a/examples/examples_nodejs_test.go +++ b/examples/examples_nodejs_test.go @@ -34,15 +34,6 @@ func TestAppSvc(t *testing.T) { integration.ProgramTest(t, &test) } -func TestECSCluster(t *testing.T) { - test := getJSBaseOptions(t). - With(integration.ProgramTestOptions{ - Dir: filepath.Join(getCwd(t), "ecscluster"), - }) - - integration.ProgramTest(t, &test) -} - func TestAppRunner(t *testing.T) { test := getJSBaseOptions(t). With(integration.ProgramTestOptions{ @@ -110,6 +101,17 @@ func TestCloudFront(t *testing.T) { integration.ProgramTest(t, &test) } +func TestLookups(t *testing.T) { + test := getJSBaseOptions(t). + With(integration.ProgramTestOptions{ + Dir: filepath.Join(getCwd(t), "lookups"), + Config: map[string]string{ + "zoneName": "coolcompany.io", + }, + }) + + integration.ProgramTest(t, &test) +} func TestEventBridgeSNS(t *testing.T) { test := getJSBaseOptions(t). diff --git a/examples/fargate/index.ts b/examples/fargate/index.ts index bcbc98cf..372bc81a 100644 --- a/examples/fargate/index.ts +++ b/examples/fargate/index.ts @@ -10,8 +10,8 @@ import { CfnTargetGroup } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; class FargateStack extends pulumicdk.Stack { loadBalancerDNS: pulumi.Output; - constructor(id: string, options?: pulumicdk.StackOptions) { - super(id, options); + constructor(app: pulumicdk.App, id: string) { + super(app, id); // Create VPC and Fargate Cluster // NOTE: Limit AZs to avoid reaching resource quotas @@ -46,11 +46,17 @@ class FargateStack extends pulumicdk.Stack { }); this.loadBalancerDNS = this.asOutput(fargateService.loadBalancer.loadBalancerDnsName); + } +} - // Finalize the stack and deploy its resources. - this.synth(); +class MyApp extends pulumicdk.App { + constructor() { + super('app', (scope: pulumicdk.App): pulumicdk.AppOutputs => { + const stack = new FargateStack(scope, 'fargatestack'); + return { loadBalancerURL: stack.loadBalancerDNS }; + }); } } -const stack = new FargateStack('fargatestack'); -export const loadBalancerURL = stack.loadBalancerDNS; +const app = new MyApp(); +export const loadBalancerURL = app.outputs['loadBalancerURL']; diff --git a/examples/lookups/Pulumi.yaml b/examples/lookups/Pulumi.yaml new file mode 100644 index 00000000..277f42ad --- /dev/null +++ b/examples/lookups/Pulumi.yaml @@ -0,0 +1,3 @@ +name: pulumi-lookups +runtime: nodejs +description: A minimal TypeScript Pulumi program diff --git a/examples/lookups/index.ts b/examples/lookups/index.ts new file mode 100644 index 00000000..fef0aed2 --- /dev/null +++ b/examples/lookups/index.ts @@ -0,0 +1,147 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as aws from '@pulumi/aws'; +import * as pulumicdk from '@pulumi/cdk'; +import * as native from '@pulumi/aws-native'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import { + aws_elasticloadbalancingv2, + aws_elasticloadbalancingv2_targets, + aws_route53, + aws_route53_targets, + CfnOutput, +} from 'aws-cdk-lib'; + +const config = new pulumi.Config(); +const zoneName = config.require('zoneName'); + +export class Ec2CdkStack extends pulumicdk.Stack { + constructor(app: pulumicdk.App, id: string) { + super(app, id, { + props: { + env: { region: aws.config.region }, + }, + }); + + // Create new VPC with 2 Subnets + const vpc = new ec2.Vpc(this, 'VPC', { + natGateways: 0, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'asterisk', + subnetType: ec2.SubnetType.PUBLIC, + }, + ], + }); + + // use getAmiOutput to lookup the AMI instead of ec2.LookupMachineImage + const ami = aws.ec2.getAmiOutput({ + owners: ['amazon'], + mostRecent: true, + filters: [ + { + name: 'name', + values: ['al2023-ami-2023.*.*.*.*-arm64'], + }, + ], + }); + + const region = aws.config.requireRegion(); + const machineImage = ec2.MachineImage.genericLinux({ + [region]: pulumicdk.asString(ami.imageId), + }); + + const instance = new ec2.Instance(this, 'Instance', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO), + machineImage, + }); + + const lb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(this, 'lb', { + vpc, + }); + + const listener = lb.addListener('http', { + protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP, + }); + + const tg = listener.addTargets('instance', { + protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP, + targets: [new aws_elasticloadbalancingv2_targets.InstanceTarget(instance)], + }); + // workaround for https://github.com/pulumi/pulumi-cdk/issues/62 + const cfnTargetGroup = tg.node.defaultChild as aws_elasticloadbalancingv2.CfnTargetGroup; + cfnTargetGroup.overrideLogicalId('LBListenerTG'); + + // use pulumi getZoneOutput and HostedZone.fromHostedZoneAttributes instead of HostedZone.fromLookup + const zone = aws.route53.getZoneOutput( + { + name: zoneName, + }, + { parent: app }, + ); + + const hostedZone = aws_route53.HostedZone.fromHostedZoneAttributes(this, 'hosted-zone', { + zoneName: pulumicdk.asString(zone.name), + hostedZoneId: pulumicdk.asString(zone.zoneId), + }); + + new aws_route53.AaaaRecord(this, 'record', { + zone: hostedZone, + target: aws_route53.RecordTarget.fromAlias(new aws_route53_targets.LoadBalancerTarget(lb)), + }); + + // use pulumi native resources side-by-side with CDK resources + new native.ssm.Parameter( + 'instance-param', + { + value: this.asOutput(instance.instanceId), + type: 'String', + }, + { parent: app }, + ); + new native.ssm.Parameter( + 'image-param', + { + value: this.asOutput(machineImage.getImage(this).imageId), + type: 'String', + }, + { parent: app }, + ); + + new CfnOutput(this, 'instanceId', { value: instance.instanceId }); + new CfnOutput(this, 'imageId', { value: machineImage.getImage(this).imageId }); + } +} + +const app = new pulumicdk.App( + 'app', + (scope: pulumicdk.App) => { + new Ec2CdkStack(scope, 'teststack'); + }, + { + remapCloudControlResource(logicalId, typeName, props, options) { + switch (typeName) { + case 'AWS::Route53::RecordSet': + return new aws.route53.Record(logicalId, { + zoneId: props.HostedZoneId, + aliases: [ + { + name: props.AliasTarget.DNSName, + zoneId: props.AliasTarget.HostedZoneId, + evaluateTargetHealth: props.AliasTarget.EvaluateTargetHealth ?? false, + }, + ], + name: props.Name, + type: props.Type, + records: props.ResourceRecords, + }); + default: + return undefined; + } + }, + }, +); + +export const imageId = app.outputs['imageId']; +export const instanceId = app.outputs['instanceId']; diff --git a/examples/lookups/package.json b/examples/lookups/package.json new file mode 100644 index 00000000..ae1d6b3f --- /dev/null +++ b/examples/lookups/package.json @@ -0,0 +1,12 @@ +{ + "name": "pulumi-aws-cdk", + "devDependencies": { + "@types/node": "^10.0.0" + }, + "dependencies": { + "@pulumi/aws-native": "^1.0.2", + "@pulumi/cdk": "^0.5.0", + "aws-cdk-lib": "2.149.0", + "constructs": "^10.0.111" + } +} diff --git a/examples/lookups/tsconfig.json b/examples/lookups/tsconfig.json new file mode 100644 index 00000000..2666e28e --- /dev/null +++ b/examples/lookups/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "strict": true, + "outDir": "bin", + "target": "es2016", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.ts" + ] +} \ No newline at end of file diff --git a/examples/s3-object-lambda/index.ts b/examples/s3-object-lambda/index.ts index 4754b8bb..0b661160 100644 --- a/examples/s3-object-lambda/index.ts +++ b/examples/s3-object-lambda/index.ts @@ -1,7 +1,19 @@ +import * as pulumi from '@pulumi/pulumi'; +import * as pulumicdk from '@pulumi/cdk'; +import type { AppOutputs } from '@pulumi/cdk'; import { S3ObjectLambdaStack } from './src/s3-object-lambda-stack'; -const s = new S3ObjectLambdaStack('stack'); -export const exampleBucketArn = s.exampleBucketArn; -export const objectLambdaArn = s.objectLambdaArn; -export const objectLambdaAccessPointArn = s.objectLambdaAccessPointArn; -export const objectLambdaAccessPointUrl = s.objectLambdaAccessPointUrl; +const app = new pulumicdk.App('app', (scope: pulumicdk.App): AppOutputs => { + const s = new S3ObjectLambdaStack(scope, 'stack'); + return { + exampleBucketArn: s.exampleBucketArn, + objectLambdaArn: s.objectLambdaArn, + objectLambdaAccessPointArn: s.objectLambdaAccessPointArn, + objectLambdaAccessPointUrl: s.objectLambdaAccessPointUrl, + }; +}); +export const exampleBucketArn = app.outputs['exampleBucketArn']; +export const objectLambdaArn = app.outputs['objectLambdaArn']; +export const objectLambdaAccessPointArn = app.outputs['objectLambdaAccessPointArn']; +export const objectLambdaAccessPointUrl = app.outputs['objectLambdaAccessPointUrl']; +export const bucketName = app.outputs['BucketName']; diff --git a/examples/s3-object-lambda/src/s3-object-lambda-stack.ts b/examples/s3-object-lambda/src/s3-object-lambda-stack.ts index 84398f3d..ccf009be 100644 --- a/examples/s3-object-lambda/src/s3-object-lambda-stack.ts +++ b/examples/s3-object-lambda/src/s3-object-lambda-stack.ts @@ -17,8 +17,8 @@ export class S3ObjectLambdaStack extends pulumicdk.Stack { objectLambdaAccessPointArn: pulumi.Output; objectLambdaAccessPointUrl: pulumi.Output; - constructor(id: string) { - super(id); + constructor(app: pulumicdk.App, id: string) { + super(app, id); const accessPoint = `arn:aws:s3:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:accesspoint/${S3_ACCESS_POINT_NAME}`; @@ -108,13 +108,12 @@ export class S3ObjectLambdaStack extends pulumicdk.Stack { }, }); + new cdk.CfnOutput(this, 'BucketName', { value: bucket.bucketName }); this.exampleBucketArn = this.asOutput(bucket.bucketArn); this.objectLambdaArn = this.asOutput(retrieveTransformedObjectLambda.functionArn); this.objectLambdaAccessPointArn = this.asOutput(objectLambdaAP.attrArn); this.objectLambdaAccessPointUrl = this.asOutput( `https://console.aws.amazon.com/s3/olap/${cdk.Aws.ACCOUNT_ID}/${OBJECT_LAMBDA_ACCESS_POINT_NAME}?region=${cdk.Aws.REGION}`, ); - - this.synth(); } } diff --git a/package.json b/package.json index 2752adec..ff2caa76 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "constructs": "^10.0.111" }, "dependencies": { + "@aws-cdk/cli-lib-alpha": "^2.161.1-alpha.0", "@types/glob": "^8.1.0", "archiver": "^7.0.1", "cdk-assets": "^2.154.8", diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts index 65cade6c..cc7c35fc 100644 --- a/src/converters/app-converter.ts +++ b/src/converters/app-converter.ts @@ -1,8 +1,8 @@ import * as pulumi from '@pulumi/pulumi'; import { AssemblyManifestReader, StackManifest } from '../assembly'; import { ConstructInfo, GraphBuilder } from '../graph'; -import { StackComponentResource, lift, Mapping } from '../types'; import { ArtifactConverter } from './artifact-converter'; +import { lift, Mapping, AppComponent, PulumiStack } from '../types'; import { CdkConstruct, ResourceMapping } from '../interop'; import { debug } from '@pulumi/pulumi/log'; import { @@ -30,7 +30,7 @@ export class AppConverter { public readonly manifestReader: AssemblyManifestReader; - constructor(readonly host: StackComponentResource) { + constructor(readonly host: AppComponent) { this.manifestReader = AssemblyManifestReader.fromDirectory(host.assemblyDir); } @@ -90,6 +90,7 @@ export class StackConverter extends ArtifactConverter { readonly parameters = new Map(); readonly resources = new Map>(); readonly constructs = new Map(); + private readonly cdkStack: PulumiStack; private _stackResource?: CdkConstruct; @@ -100,8 +101,9 @@ export class StackConverter extends ArtifactConverter { return this._stackResource; } - constructor(private readonly host: StackComponentResource, readonly stack: StackManifest) { + constructor(host: AppComponent, readonly stack: StackManifest) { super(host); + this.cdkStack = host.stacks[stack.id]; } public convert(dependencies: Set) { @@ -114,18 +116,14 @@ export class StackConverter extends ArtifactConverter { for (const n of dependencyGraphNodes) { if (n.construct.id === this.stack.id) { - this._stackResource = new CdkConstruct( - `${this.stackComponent.name}/${n.construct.path}`, - n.construct.id, - { - parent: this.stackComponent.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 - // doesn't track asset dependencies at that level - dependsOn: this.stackDependsOn(dependencies), - }, - ); + this._stackResource = new CdkConstruct(`${this.app.name}/${n.construct.path}`, n.construct.id, { + parent: this.app, + // 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 + // doesn't track asset dependencies at that level + dependsOn: this.stackDependsOn(dependencies), + }); this.constructs.set(n.construct, this._stackResource); continue; } @@ -155,18 +153,13 @@ export class StackConverter extends ArtifactConverter { // // Do something with the condition // } } else { - const r = new CdkConstruct(`${this.stackComponent.name}/${n.construct.path}`, n.construct.type, { + const r = new CdkConstruct(`${this.app.name}/${n.construct.path}`, n.construct.type, { parent, }); this.constructs.set(n.construct, r); } } - // Register the outputs as outputs of the component resource. - for (const [outputId, args] of Object.entries(this.stack.outputs ?? {})) { - this.stackComponent.registerOutput(outputId, this.processIntrinsics(args.Value)); - } - for (let i = dependencyGraphNodes.length - 1; i >= 0; i--) { const n = dependencyGraphNodes[i]; if (!n.resource) { @@ -209,7 +202,7 @@ export class StackConverter extends ArtifactConverter { return key; } - this.parameters.set(logicalId, parameterValue(this.stackComponent.component)); + this.parameters.set(logicalId, parameterValue(this.app)); } private mapResource( @@ -218,8 +211,8 @@ export class StackConverter extends ArtifactConverter { props: any, options: pulumi.ResourceOptions, ): ResourceMapping[] { - if (this.stackComponent.options?.remapCloudControlResource !== undefined) { - const res = this.stackComponent.options.remapCloudControlResource(logicalId, typeName, props, options); + if (this.app.appOptions?.remapCloudControlResource !== undefined) { + const res = this.app.appOptions.remapCloudControlResource(logicalId, typeName, props, options); if (res !== undefined) { debug(`remapped ${logicalId}`); return res; @@ -247,11 +240,11 @@ export class StackConverter extends ArtifactConverter { /** @internal */ asOutputValue(v: T): T { - const value = this.stackComponent.stack.resolve(v); + const value = this.cdkStack.resolve(v); return this.processIntrinsics(value) as T; } - private processIntrinsics(obj: any): any { + public processIntrinsics(obj: any): any { try { debug(`Processing intrinsics for ${JSON.stringify(obj)}`); } catch { @@ -370,15 +363,15 @@ export class StackConverter extends ArtifactConverter { switch (target) { case 'AWS::AccountId': - return getAccountId({ parent: this.stackComponent.component }).then((r) => r.accountId); + return getAccountId({ parent: this.app }).then((r) => r.accountId); case 'AWS::NoValue': return undefined; case 'AWS::Partition': - return getPartition({ parent: this.stackComponent.component }).then((p) => p.partition); + return getPartition({ parent: this.app }).then((p) => p.partition); case 'AWS::Region': - return getRegion({ parent: this.stackComponent.component }).then((r) => r.region); + return getRegion({ parent: this.app }).then((r) => r.region); case 'AWS::URLSuffix': - return getUrlSuffix({ parent: this.stackComponent.component }).then((r) => r.urlSuffix); + return getUrlSuffix({ parent: this.app }).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 0725ed0d..0bc2bee6 100644 --- a/src/converters/artifact-converter.ts +++ b/src/converters/artifact-converter.ts @@ -6,7 +6,7 @@ import { StackComponentResource } from '../types'; * ArtifactConverter */ export abstract class ArtifactConverter { - constructor(protected readonly stackComponent: StackComponentResource) {} + constructor(protected readonly app: AppComponent) {} /** * Takes a string and resolves any CDK environment placeholders (e.g. accountId, region, partition) @@ -15,7 +15,7 @@ export abstract class ArtifactConverter { * @returns The string with the placeholders fully resolved */ protected resolvePlaceholders(s: string): Promise { - const host = this.stackComponent; + const host = this.app; return cx.EnvironmentPlaceholders.replaceAsync(s, { async region(): Promise { return getRegion({ parent: host.component }).then((r) => r.region); diff --git a/src/stack.ts b/src/stack.ts index 90fb7d79..99379c0b 100644 --- a/src/stack.ts +++ b/src/stack.ts @@ -14,23 +14,23 @@ import * as cdk from 'aws-cdk-lib'; import * as cx from 'aws-cdk-lib/cx-api'; import * as pulumi from '@pulumi/pulumi'; -import { debug } from '@pulumi/pulumi/log'; -import { StackComponentResource, StackOptions } from './types'; +import { AppComponent, AppOptions, PulumiStack } from './types'; import { AppConverter, StackConverter } from './converters/app-converter'; import { PulumiSynthesizer } from './synthesizer'; import { CdkConstruct } from './interop'; +import { AwsCdkCli, ICloudAssemblyDirectoryProducer } from '@aws-cdk/cli-lib-alpha'; +import { error } from '@pulumi/pulumi/log'; -/** - * StackComponentResource is the underlying pulumi ComponentResource for each pulumicdk.Stack - * 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 - */ -class StackComponent extends pulumi.ComponentResource implements StackComponentResource { - /** @internal */ - name: string; +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; /** @internal */ - converter: AppConverter; + public converter: Promise; /** * @internal @@ -42,44 +42,110 @@ class StackComponent extends pulumi.ComponentResource implements StackComponentR * @internal */ public assemblyDir: string; + private _app?: cdk.App; /** - * Any stack options that are supplied by the user * @internal */ - public options?: StackOptions; + public appOptions?: AppOptions; + + public get app(): cdk.App { + if (!this._app) { + throw new Error('cdk.App has not been created yet'); + } + return this._app!; + } /** - * @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. */ - public dependencies: CdkConstruct[] = []; + public outputs: { [outputId: string]: pulumi.Output } = {}; + + private readonly createFunc: (scope: App) => AppOutputs | void; + private appProps?: cdk.AppProps; - constructor(public readonly stack: Stack) { - super('cdk:index:Stack', stack.node.id, {}, stack.options); - this.options = stack.options; - this.dependencies.push(stack.pulumiSynthesizer.stagingStack); + constructor(id: string, createFunc: (scope: App) => void | AppOutputs, props?: AppOptions) { + super(id, props); + this.appOptions = props; + this.createFunc = createFunc; - this.name = stack.node.id; + this.name = id; + this.appProps = props?.props; + this.converter = this.getData(); - const assembly = stack.app.synth(); - this.assemblyDir = assembly.directory; - debug(`ASSEMBLY_DIR: ${this.assemblyDir}`); + const outputs = this.converter.then((converter) => { + const stacks = Array.from(converter.stacks.values()); + return stacks.reduce( + (prev, curr) => { + const o: { [outputId: string]: pulumi.Output } = {}; + for (const [outputId, args] of Object.entries(curr.stack.outputs ?? {})) { + o[outputId] = curr.processIntrinsics(args.Value); + } + return { + ...prev, + ...o, + }; + }, + { ...this.outputs } as pulumi.Output<{ [outputId: string]: pulumi.Output }>, + ); + }); + this.outputs = pulumi.output(outputs); + this.registerOutputs(this.outputs); + } - debug(JSON.stringify(debugAssembly(assembly))); + protected async initialize(): Promise { + const cli = AwsCdkCli.fromCloudAssemblyDirectoryProducer(this); + try { + await cli.synth({ quiet: true, lookups: false }); + } catch (e: any) { + if (typeof e.message === 'string' && e.message.includes('Context lookups have been disabled')) { + const message = e.message as string; + const messageParts = message.split('Context lookups have been disabled. '); + const missingParts = messageParts[1].split('Missing context keys: '); + error( + 'Context lookups have been disabled. Make sure all necessary context is already in "cdk.context.json". \n' + + 'Missing context keys: ' + + missingParts[1], + this, + ); + } else { + error(e.message, this); + } + } - this.converter = new AppConverter(this); - this.converter.convert(); + const converter = new AppConverter(this); + converter.convert(); - this.registerOutputs(stack.outputs); - this.component = this; + return converter; } - /** @internal */ - registerOutput(outputId: string, output: any) { - this.stack.outputs[outputId] = pulumi.output(output); + async produce(context: Record): Promise { + const app = new cdk.App({ + ...(this.appProps ?? {}), + autoSynth: false, + analyticsReporting: false, + context, + }); + this._app = app; + this.assemblyDir = app.outdir; + const outputs = this.createFunc(this); + this.outputs = outputs ?? {}; + + app.node.children.forEach((child) => { + if (Stack.isPulumiStack(child)) { + this.stacks[child.artifactId] = child; + } + }); + + return app.synth().directory; } } +export interface StackOptions extends pulumi.ComponentResourceOptions { + props: cdk.StackProps; +} + /** * A Construct that represents an AWS CDK stack deployed with Pulumi. * @@ -87,31 +153,40 @@ class StackComponent extends pulumi.ComponentResource implements StackComponentR * all CDK resources have been defined in order to deploy the stack (usually, this is done as the last line of the * subclass's constructor). */ -export class Stack extends cdk.Stack { - // The URN of the underlying Pulumi component. - urn!: pulumi.Output; - resolveURN!: (urn: pulumi.Output) => void; - rejectURN!: (error: any) => void; - +export class Stack extends PulumiStack { /** - * 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. + * Return whether the given object is a Stack. + * + * We do attribute detection since we can't reliably use 'instanceof'. + * @internal */ - outputs: { [outputId: string]: pulumi.Output } = {}; + public static isPulumiStack(x: any): x is Stack { + return x !== null && typeof x === 'object' && STACK_SYMBOL in x; + } - /** @internal */ - app: cdk.App; + // // The URN of the underlying Pulumi component. + // urn!: pulumi.Output; + // resolveURN!: (urn: pulumi.Output) => void; + // rejectURN!: (error: any) => void; /** @internal */ - options: StackOptions | undefined; + app: cdk.App; // The stack's converter. This is used by asOutput in order to convert CDK values to Pulumi Outputs. This is a // Promise so users are able to call asOutput before they've called synth. Note that this _does_ make forgetting // to call synth a sharper edge: calling asOutput without calling synth will create outputs that never resolve // and the program will hang. converter!: Promise; - resolveConverter!: (converter: StackConverter) => void; - rejectConverter!: (error: any) => void; + + /** + * @internal + */ + public options?: StackOptions; + + /** @internal */ + public outdir: string; + + private pulumiApp: App; /** * @internal @@ -124,59 +199,17 @@ export class Stack extends cdk.Stack { * @param name The _unique_ name of the resource. * @param options A bag of options that control this resource's behavior. */ - constructor(name: string, options?: StackOptions) { - const appId = options?.appId ?? generateAppId(); + constructor(app: App, name: string, options?: StackOptions) { + super(app.app, name, options?.props); + Object.defineProperty(this, STACK_SYMBOL, { value: true }); - // TODO: allow the user to customize this https://github.com/pulumi/pulumi-cdk/issues/180 - const synthesizer = new PulumiSynthesizer({ - appId, - }); - const app = new cdk.App({ - defaultStackSynthesizer: synthesizer, - context: { - // Ask CDK to attach 'aws:asset:*' metadata to resources in generated stack templates. Although this - // metadata is not currently used, it may be useful in the future to map between assets and the - // resources with which they are associated. For example, the lambda.Function L2 construct attaches - // metadata for its Code asset (if any) to its generated CFN resource. - [cx.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: true, - - // Ask CDK to embed 'aws:cdk:path' metadata in resources in generated stack templates. Although this - // metadata is not currently used, it provides an aditional mechanism by which we can map between - // constructs and the resources they emit in the CFN template. - [cx.PATH_METADATA_ENABLE_CONTEXT]: true, - }, - }); - - super(app, name, options?.props); - this.pulumiSynthesizer = synthesizer; - - this.app = app; + this.pulumiApp = app; this.options = options; - const urnPromise = new Promise((resolve, reject) => { - this.resolveURN = resolve; - this.rejectURN = reject; - }); - this.urn = pulumi.output(urnPromise); + this.outdir = app.assemblyDir; + this.app = app.app; - this.converter = new Promise((resolve, reject) => { - this.resolveConverter = resolve; - this.rejectConverter = reject; - }); - } - - /** - * Finalize the stack and deploy its resources. - */ - protected synth() { - try { - const component = new StackComponent(this); - this.resolveURN(component.urn); - this.resolveConverter(component.converter.stacks.get(this.artifactId)!); - } catch (e) { - this.rejectURN(e); - this.rejectConverter(e); - } + this.converter = this.pulumiApp.converter.then((converter) => converter.stacks.get(this.artifactId)!); } /** diff --git a/src/types.ts b/src/types.ts index 9785ef7c..1ff12951 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,55 @@ 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'; + +export abstract class PulumiStack extends Stack { + /** + * 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. + */ + public readonly outputs: { [outputId: string]: pulumi.Output } = {}; + + constructor(app: App, name: string, options?: StackProps) { + super(app, name, options); + } + /** @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. */ @@ -59,12 +108,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 interface StackComponentResource { - /** - * The name of the component resource - * @internal - */ - name: string; +export abstract class AppComponent extends pulumi.ComponentResource { + public abstract name: string; /** * The directory to which cdk synthesizes the CloudAssembly @@ -75,31 +120,16 @@ export interface StackComponentResource { /** * The CDK stack associated with the component resource */ - readonly stack: Stack; + public readonly stacks: { [artifactId: string]: PulumiStack } = {}; /** - * Any stack options that are supplied by the user * @internal */ - options?: StackOptions; + public abstract appOptions?: AppOptions; - /** - * The Resources that the component resource depends on - * This will typically be the staging resources - * - * @internal - */ - readonly dependencies: CdkConstruct[]; - - /** - * @internal - */ - readonly component: pulumi.ComponentResource; - - /** - * @internal - */ - registerOutput(outputId: string, outupt: any): void; + constructor(id: string, options?: AppOptions) { + super('cdk:index:App', id, options?.props, options); + } } export type Mapping = { diff --git a/tests/assembly/manifest.test.ts b/tests/assembly/manifest.test.ts index a935ee4b..dd500d05 100644 --- a/tests/assembly/manifest.test.ts +++ b/tests/assembly/manifest.test.ts @@ -10,7 +10,13 @@ describe('cloud assembly manifest reader', () => { beforeEach(() => { mockfs({ // Recursively loads all node_modules - node_modules: mockfs.load(path.resolve(__dirname, '../../node_modules')), + node_modules: { + 'aws-cdk-lib': mockfs.load(path.resolve(__dirname, '../../node_modules/aws-cdk-lib')), + '@pulumi': { + aws: mockfs.load(path.resolve(__dirname, '../../node_modules/@pulumi/aws')), + 'aws-native': mockfs.load(path.resolve(__dirname, '../../node_modules/@pulumi/aws-native')), + }, + }, [manifestAssets]: JSON.stringify({ version: '36.0.0', files: { @@ -141,7 +147,7 @@ describe('cloud assembly manifest reader', () => { 'test-stack/MyFunction1/Resource': 'MyFunction12A744C2E', 'test-stack/MyFunction1/ServiceRole/Resource': 'MyFunction1ServiceRole9852B06B', }, - outputs: undefined, + outputs: {}, parameters: undefined, resources: { MyFunction12A744C2E: { Properties: {}, Type: 'AWS::Lambda::Function' }, diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 3c788a02..bbb1cc4e 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -13,50 +13,23 @@ // limitations under the License. import * as pulumi from '@pulumi/pulumi'; import * as s3 from 'aws-cdk-lib/aws-s3'; -import { Stack } from '../src/stack'; -import { Construct } from 'constructs'; +import { App, Stack } from '../src/stack'; import * as output from '../src/output'; import { promiseOf, setMocks } 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'; -function testStack(id: string, fn: (scope: Construct) => void): Stack { - class TestStack extends Stack { - constructor(id: string) { - super(id); - - fn(this); - - this.synth(); - } - } - - const s = new TestStack(id); - return s; -} - -beforeAll(() => { - setMocks(); -}); - describe('Basic tests', () => { test('Checking single resource registration', async () => { - const stack = testStack('test1', (adapter) => { - new s3.Bucket(adapter, 'MyFirstBucket', { versioned: true }); + const app = new App('testapp', (scope: App) => { + const s = new Stack(scope, 'teststack'); + new s3.Bucket(s, 'MyFirstBucket', { versioned: true }); }); - const urn = await promiseOf(stack.urn); - expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:Stack::test1'); + const urn = await promiseOf(app.urn); + expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:App::testapp'); }); - test('Supports Output', async () => { - const o = pulumi.output('the-bucket-name'); - const stack = testStack('test2', (adapter) => { - new s3.Bucket(adapter, 'MyFirstBucket', { bucketName: output.asString(o) }); - }); - const urn = await promiseOf(stack.urn); - expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:Stack::test2'); - }); test('LoadBalancer dnsName attribute does not throw', async () => { const stack = testStack('test3', (scope) => { const vpc = new Vpc(scope, 'vpc'); @@ -74,4 +47,13 @@ describe('Basic tests', () => { 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) }); + }); + 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 6e4ad78f..fdfa6523 100644 --- a/tests/cdk-resource.test.ts +++ b/tests/cdk-resource.test.ts @@ -3,6 +3,12 @@ import { TableArgs } from '@pulumi/aws-native/dynamodb'; import { Key } from 'aws-cdk-lib/aws-kms'; import { setMocks, testStack } 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'; describe('CDK Construct tests', () => { // DynamoDB table was previously mapped to the `aws` provider @@ -12,9 +18,58 @@ describe('CDK Construct tests', () => { const resources: MockResourceArgs[] = []; setMocks(resources); - await testStack((scope) => { - const key = Key.fromKeyArn(scope, 'key', 'arn:aws:kms:us-west-2:123456789012:key/abcdefg'); - const table = new dynamodb.Table(scope, 'Table', { + 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', { encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED, encryptionKey: key, sortKey: { @@ -41,52 +96,27 @@ describe('CDK Construct tests', () => { }, }); }); + 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); + 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'); }); }); diff --git a/tests/converters/app-converter.test.ts b/tests/converters/app-converter.test.ts index 23377f20..5a140086 100644 --- a/tests/converters/app-converter.test.ts +++ b/tests/converters/app-converter.test.ts @@ -1,28 +1,24 @@ import { AppConverter, StackConverter } from '../../src/converters/app-converter'; import { Stack } from 'aws-cdk-lib/core'; -import { StackComponentResource, StackOptions } from '../../src/types'; +import { AppComponent, AppOptions } 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 pulumi.ComponentResource implements StackComponentResource { +class MockStackComponent extends AppComponent { public readonly name = 'stack'; public readonly assemblyDir: string; component: pulumi.ComponentResource; public stack: Stack; - public options?: StackOptions | undefined; - public dependencies: CdkConstruct[] = []; + public appOptions?: AppOptions | undefined; constructor(dir: string) { - super('cdk:index:Stack', 'stack', {}, {}); + super('stack'); this.assemblyDir = dir; this.registerOutputs(); } - - registerOutput(outputId: string, output: any): void {} } beforeAll(() => { @@ -37,7 +33,13 @@ describe('App Converter', () => { beforeEach(() => { mockfs({ // Recursively loads all node_modules - node_modules: mockfs.load(path.resolve(__dirname, '../../node_modules')), + node_modules: { + 'aws-cdk-lib': mockfs.load(path.resolve(__dirname, '../../node_modules/aws-cdk-lib')), + '@pulumi': { + aws: mockfs.load(path.resolve(__dirname, '../../node_modules/@pulumi/aws')), + 'aws-native': mockfs.load(path.resolve(__dirname, '../../node_modules/@pulumi/aws-native')), + }, + }, [manifestAssets]: JSON.stringify({ version: '36.0.0', files: { diff --git a/tests/converters/artifact-converter.test.ts b/tests/converters/artifact-converter.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/tests/mocks.ts b/tests/mocks.ts index 9fdcd7bd..792ea2cd 100644 --- a/tests/mocks.ts +++ b/tests/mocks.ts @@ -1,36 +1,11 @@ import * as pulumi from '@pulumi/pulumi'; -import { Stack } from '../src/stack'; -import { Construct } from 'constructs'; -import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime'; +import { MockCallArgs, MockResourceArgs, setMockOptions } from '@pulumi/pulumi/runtime'; +import { MockMonitor } from '@pulumi/pulumi/runtime/mocks'; // 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 testStack(fn: (scope: Construct) => void) { - class TestStack extends Stack { - constructor(id: string) { - super(id, { - props: { - env: { - region: 'us-east-1', - account: '12345678912', - }, - }, - }); - - fn(this); - - this.synth(); - } - } - - const s = new TestStack('teststack'); - const converter = await s.converter; - await Promise.all(Array.from(converter.constructs.values()).flatMap((v) => promiseOf(v.urn))); - await promiseOf(s.urn); - await promiseOf(s.pulumiSynthesizer.stagingStack.urn); -} export function setMocks(resources?: MockResourceArgs[]) { const mocks: pulumi.runtime.Mocks = { diff --git a/yarn.lock b/yarn.lock index 532d08d0..9fbaa233 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,11 @@ resolved "https://registry.yarnpkg.com/@aws-cdk/aws-apprunner-alpha/-/aws-apprunner-alpha-2.20.0-alpha.0.tgz#66ae8b2795281bf46163872f450d9163cf4beb39" integrity sha512-Eno+FXxa7k0Irx9ssl0ML44rlBg2THo8WMqxO3dKZpAZeZbfd8s8T3/UjP1Fq22TCKn+psDJ+wiUAd9r/BI2ig== +"@aws-cdk/cli-lib-alpha@^2.161.1-alpha.0": + version "2.161.1-alpha.0" + resolved "https://registry.yarnpkg.com/@aws-cdk/cli-lib-alpha/-/cli-lib-alpha-2.161.1-alpha.0.tgz#f00f5190f7da2e8f62807c5a01fb629298c767f4" + integrity sha512-HCokBr85Msv0tXiKth/3ZJZaQLzMmydk3NNEEA9fD/tzBh1zUcnlsBQnclOBmd0uKMNSZQertrroJmZv3mBOeg== + "@aws-cdk/cloud-assembly-schema@^38.0.1": version "38.0.1" resolved "https://registry.yarnpkg.com/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz#cdf4684ae8778459e039cd44082ea644a3504ca9"