From add304418434bdd92a66ad987a53b38b06a0cfde Mon Sep 17 00:00:00 2001 From: Cory Hall <43035978+corymhall@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:13:41 -0500 Subject: [PATCH] Fix IPV6 cidr blocks (#197) Depends on https://github.com/pulumi/pulumi-aws-native/pull/1797 Because of https://github.com/pulumi/pulumi-aws-native/issues/1798 we need to add a special case for Ipv6 cidr blocks that are added to the VPC via a `VPCCidrBlock` resource. This PR adds special handling where we track both the `Vpc` and the `VpcCidrBlock` resource and swap any references. --- integration/ec2/Pulumi.yaml | 3 + integration/ec2/index.ts | 127 ++++++++++++++++ integration/ec2/package.json | 15 ++ integration/ec2/tsconfig.json | 18 +++ integration/examples_nodejs_test.go | 9 ++ package.json | 2 +- src/converters/app-converter.ts | 38 +++-- src/graph.ts | 98 +++++++++++- tests/converters/app-converter.test.ts | 197 ++++++++++++++++++++++++- tests/graph.test.ts | 163 +++++++++++++++++++- yarn.lock | 8 +- 11 files changed, 649 insertions(+), 29 deletions(-) create mode 100644 integration/ec2/Pulumi.yaml create mode 100644 integration/ec2/index.ts create mode 100644 integration/ec2/package.json create mode 100644 integration/ec2/tsconfig.json diff --git a/integration/ec2/Pulumi.yaml b/integration/ec2/Pulumi.yaml new file mode 100644 index 00000000..d876e339 --- /dev/null +++ b/integration/ec2/Pulumi.yaml @@ -0,0 +1,3 @@ +name: pulumi-aws-ec2 +runtime: nodejs +description: ec2 integration test diff --git a/integration/ec2/index.ts b/integration/ec2/index.ts new file mode 100644 index 00000000..b8d3f471 --- /dev/null +++ b/integration/ec2/index.ts @@ -0,0 +1,127 @@ +import * as aws from '@pulumi/aws'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import * as pulumicdk from '@pulumi/cdk'; +import { SecretValue } from 'aws-cdk-lib/core'; + +class Ec2Stack extends pulumicdk.Stack { + constructor(app: pulumicdk.App, id: string, options?: pulumicdk.StackOptions) { + super(app, id, options); + const vpc = new ec2.Vpc(this, 'Vpc', { + maxAzs: 2, + ipProtocol: ec2.IpProtocol.DUAL_STACK, + vpnGateway: true, + ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'), + natGateways: 1, + vpnConnections: { + dynamic: { + ip: '1.2.3.4', + tunnelOptions: [ + { + preSharedKeySecret: SecretValue.unsafePlainText('secretkey1234'), + }, + { + preSharedKeySecret: SecretValue.unsafePlainText('secretkey5678'), + }, + ], + }, + static: { + ip: '4.5.6.7', + staticRoutes: ['192.168.10.0/24', '192.168.20.0/24'], + }, + }, + subnetConfiguration: [ + { + name: 'Public', + subnetType: ec2.SubnetType.PUBLIC, + }, + { + name: 'Private', + subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, + }, + { + name: 'Isolated', + subnetType: ec2.SubnetType.PRIVATE_ISOLATED, + }, + ], + restrictDefaultSecurityGroup: false, + }); + + vpc.addFlowLog('FlowLogs', { + destination: ec2.FlowLogDestination.toCloudWatchLogs(), + }); + + vpc.addGatewayEndpoint('Dynamo', { + service: ec2.GatewayVpcEndpointAwsService.DYNAMODB, + }); + vpc.addInterfaceEndpoint('ecr', { + service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER, + }); + + new ec2.PrefixList(this, 'PrefixList', {}); + const nacl = new ec2.NetworkAcl(this, 'NetworkAcl', { + vpc, + subnetSelection: { subnetType: ec2.SubnetType.PUBLIC }, + }); + nacl.addEntry('AllowAll', { + cidr: ec2.AclCidr.anyIpv4(), + ruleAction: ec2.Action.ALLOW, + ruleNumber: 100, + traffic: ec2.AclTraffic.allTraffic(), + }); + new ec2.KeyPair(this, 'KeyPair'); + + const nlb = new elbv2.NetworkLoadBalancer(this, 'NLB1', { vpc }); + new ec2.VpcEndpointService(this, 'EndpointService', { + vpcEndpointServiceLoadBalancers: [nlb], + allowedPrincipals: [new iam.ArnPrincipal('ec2.amazonaws.com')], + }); + } +} + +new pulumicdk.App( + 'app', + (scope: pulumicdk.App) => { + new Ec2Stack(scope, 'teststack'); + }, + { + appOptions: { + remapCloudControlResource: (logicalId, typeName, props, options) => { + if (typeName === 'AWS::EC2::VPNGatewayRoutePropagation') { + const tableIds: string[] = props.RouteTableIds; + return tableIds.flatMap((tableId, i) => { + const id = i === 0 ? logicalId : `${logicalId}-${i}`; + return { + logicalId: id, + resource: new aws.ec2.VpnGatewayRoutePropagation( + id, + { + routeTableId: tableId, + vpnGatewayId: props.VpnGatewayId, + }, + options, + ), + }; + }); + } + if (typeName === 'AWS::EC2::NetworkAclEntry') { + return new aws.ec2.NetworkAclRule(logicalId, { + egress: props.Egress, + toPort: props.PortRange?.To, + fromPort: props.PortRange?.From, + protocol: props.Protocol, + ruleNumber: props.RuleNumber, + networkAclId: props.NetworkAclId, + ruleAction: props.RuleAction, + cidrBlock: props.CidrBlock, + ipv6CidrBlock: props.Ipv6CidrBlock, + icmpCode: props.Icmp?.Code, + icmpType: props.Icmp?.Type, + }); + } + return undefined; + }, + }, + }, +); diff --git a/integration/ec2/package.json b/integration/ec2/package.json new file mode 100644 index 00000000..b9eaf5db --- /dev/null +++ b/integration/ec2/package.json @@ -0,0 +1,15 @@ +{ + "name": "pulumi-aws-cdk", + "devDependencies": { + "@types/node": "^10.0.0" + }, + "dependencies": { + "@pulumi/aws": "^6.0.0", + "@pulumi/aws-native": "^1.6.0", + "@pulumi/cdk": "^0.5.0", + "@pulumi/pulumi": "^3.0.0", + "aws-cdk-lib": "2.149.0", + "constructs": "10.3.0", + "esbuild": "^0.24.0" + } +} diff --git a/integration/ec2/tsconfig.json b/integration/ec2/tsconfig.json new file mode 100644 index 00000000..eac442cb --- /dev/null +++ b/integration/ec2/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "strict": true, + "outDir": "bin", + "target": "es2019", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "./*.ts" + ] +} diff --git a/integration/examples_nodejs_test.go b/integration/examples_nodejs_test.go index 0832dfa8..b7469e66 100644 --- a/integration/examples_nodejs_test.go +++ b/integration/examples_nodejs_test.go @@ -41,6 +41,15 @@ func TestApiGatewayDomain(t *testing.T) { integration.ProgramTest(t, &test) } +func TestEc2(t *testing.T) { + test := getJSBaseOptions(t). + With(integration.ProgramTestOptions{ + Dir: filepath.Join(getCwd(t), "ec2"), + }) + + integration.ProgramTest(t, &test) +} + func getJSBaseOptions(t *testing.T) integration.ProgramTestOptions { base := getBaseOptions(t) baseJS := base.With(integration.ProgramTestOptions{ diff --git a/package.json b/package.json index ff2caa76..51acfea8 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@aws-cdk/aws-apprunner-alpha": "2.20.0-alpha.0", "@pulumi/aws": "^6.32.0", - "@pulumi/aws-native": "^1.0.0", + "@pulumi/aws-native": "^1.6.0", "@pulumi/docker": "^4.5.0", "@pulumi/pulumi": "3.121.0", "@types/archiver": "^6.0.2", diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts index 21cd6115..679d63f4 100644 --- a/src/converters/app-converter.ts +++ b/src/converters/app-converter.ts @@ -1,7 +1,7 @@ import * as cdk from 'aws-cdk-lib/core'; import * as pulumi from '@pulumi/pulumi'; import { AssemblyManifestReader, StackManifest } from '../assembly'; -import { ConstructInfo, GraphBuilder, GraphNode } from '../graph'; +import { ConstructInfo, Graph, GraphBuilder, GraphNode } from '../graph'; import { ArtifactConverter } from './artifact-converter'; import { lift, Mapping, AppComponent } from '../types'; import { CdkConstruct, ResourceAttributeMapping, ResourceMapping } from '../interop'; @@ -94,6 +94,7 @@ export class StackConverter extends ArtifactConverter { private readonly cdkStack: cdk.Stack; private _stackResource?: CdkConstruct; + private readonly graph: Graph; public get stackResource(): CdkConstruct { if (!this._stackResource) { @@ -105,17 +106,16 @@ export class StackConverter extends ArtifactConverter { constructor(host: AppComponent, readonly stack: StackManifest) { super(host); this.cdkStack = host.stacks[stack.id]; + this.graph = GraphBuilder.build(this.stack); } public convert(dependencies: Set) { - const dependencyGraphNodes = GraphBuilder.build(this.stack); - // process parameters first because resources will reference them for (const [logicalId, value] of Object.entries(this.stack.parameters ?? {})) { this.mapParameter(logicalId, value.Type, value.Default); } - for (const n of dependencyGraphNodes) { + for (const n of this.graph.nodes) { if (n.construct.id === this.stack.id) { this._stackResource = new CdkConstruct(`${this.app.name}/${n.construct.path}`, n.construct.id, { parent: this.app.component, @@ -156,8 +156,8 @@ export class StackConverter extends ArtifactConverter { } } - for (let i = dependencyGraphNodes.length - 1; i >= 0; i--) { - const n = dependencyGraphNodes[i]; + for (let i = this.graph.nodes.length - 1; i >= 0; i--) { + const n = this.graph.nodes[i]; if (!n.resource) { (this.constructs.get(n.construct)!).done(); } @@ -359,7 +359,23 @@ export class StackConverter extends ArtifactConverter { private resolveIntrinsic(fn: string, params: any) { switch (fn) { case 'Fn::GetAtt': { - debug(`Fn::GetAtt(${params[0]}, ${params[1]})`); + const logicalId = params[0]; + const attributeName = params[1]; + debug(`Fn::GetAtt(${logicalId}, ${attributeName})`); + // Special case for VPC Ipv6CidrBlocks + // Ipv6 cidr blocks are added to the VPC through a separate VpcCidrBlock resource + // Due to [pulumi/pulumi-aws-native#1798] the `Ipv6CidrBlocks` attribute will always be empty + // and we need to instead pull the `Ipv6CidrBlock` attribute from the VpcCidrBlock resource. + if ( + logicalId in this.graph.vpcNodes && + attributeName === 'Ipv6CidrBlocks' && + this.graph.vpcNodes[logicalId].vpcCidrBlockNode?.logicalId + ) { + return [ + this.resolveAtt(this.graph.vpcNodes[logicalId].vpcCidrBlockNode.logicalId, 'Ipv6CidrBlock'), + ]; + } + return this.resolveAtt(params[0], params[1]); } @@ -375,17 +391,17 @@ export class StackConverter extends ArtifactConverter { case 'Fn::Base64': return lift((str) => Buffer.from(str).toString('base64'), this.processIntrinsics(params)); - case 'Fn::Cidr': + case 'Fn::Cidr': { return lift( ([ipBlock, count, cidrBits]) => cidr({ ipBlock, - count, - cidrBits, + count: parseInt(count, 10), + cidrBits: parseInt(cidrBits, 10), }).then((r) => r.subnets), this.processIntrinsics(params), ); - + } case 'Fn::GetAZs': return lift(([region]) => getAzs({ region }).then((r) => r.azs), this.processIntrinsics(params)); diff --git a/src/graph.ts b/src/graph.ts index b9156657..467a0874 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -109,21 +109,55 @@ function typeFromFqn(fqn: string): PulumiResourceType { return `${mod}:${type}`; } +/** + * Represents the dependency graph of the constructs in the CDK app + */ +export interface Graph { + /** + * The nodes in the graph sorted in topological order + */ + nodes: GraphNode[]; + + /** + * The VPC nodes in the graph + */ + vpcNodes: { [logicalId: string]: VpcGraphNode }; +} + +/** + * Represents a VPC and its (optionally) associated VPCCidrBlock + */ +export interface VpcGraphNode { + /** + * The GraphNode representing the VPC + */ + vpcNode: GraphNode; + + /** + * The GraphNode representing the VPCCidrBlock + */ + vpcCidrBlockNode?: GraphNode; +} + export class GraphBuilder { // Allows for easy access to the GraphNode of a specific Construct constructNodes: Map; // Map of resource logicalId to GraphNode. Allows for easy lookup by logicalId cfnElementNodes: Map; + // If the app has a VpcCidrBlock resource, this will be set to the GraphNode representing it + private readonly _vpcCidrBlockNodes: { [logicalId: string]: GraphNode } = {}; + // If the app has a Vpc resource, this will be set to the GraphNode representing it + private readonly vpcNodes: { [logicalId: string]: VpcGraphNode } = {}; + constructor(private readonly stack: StackManifest) { this.constructNodes = new Map(); this.cfnElementNodes = new Map(); } // build constructs a dependency graph from the adapter and returns its nodes sorted in topological order. - public static build(stack: StackManifest): GraphNode[] { - const b = new GraphBuilder(stack); - return b._build(); + public static build(stack: StackManifest): Graph { + return new GraphBuilder(stack)._build(); } /** @@ -163,6 +197,12 @@ export class GraphBuilder { `Something went wrong: resourceType ${resource.Type} does not equal CfnType ${cfnType}`, ); } + if (resource.Type === 'AWS::EC2::VPCCidrBlock') { + this._vpcCidrBlockNodes[node.logicalId] = node; + } + if (resource.Type === 'AWS::EC2::VPC') { + this.vpcNodes[node.logicalId] = { vpcNode: node, vpcCidrBlockNode: undefined }; + } } this.constructNodes.set(construct, node); if (tree.children) { @@ -170,7 +210,7 @@ export class GraphBuilder { } } - private _build(): GraphNode[] { + private _build(): Graph { // passes // 1. collect all constructs into a map from construct name to DAG node, converting CFN elements to fragments // 2. hook up dependency edges @@ -181,6 +221,31 @@ export class GraphBuilder { // NOTE: this doesn't handle cross-stack references. We'll likely need to do so, at least for nested stacks. this.parseTree(this.stack.constructTree); + // parseTree does not guarantee that the VPC resource will be parsed before the VPCCidrBlock resource + // so we need to process this separately after + if (Object.keys(this._vpcCidrBlockNodes).length) { + Object.entries(this._vpcCidrBlockNodes).forEach(([logicalId, node]) => { + const resource = node.resource; + if (!resource) { + throw new Error(`Something went wrong. CFN Resource not found for VPCCidrBlock ${logicalId}`); + } + const vpcRef = resource.Properties.VpcId; + if (typeof vpcRef === 'object' && 'Ref' in vpcRef) { + const vpcLogicalId = this.cfnElementNodes.get(vpcRef.Ref)?.logicalId; + if (!vpcLogicalId) { + throw new Error(`VPC resource ${vpcRef.Ref} not found for VPCCidrBlock ${node.logicalId}`); + } + const vpcNode = this.vpcNodes[vpcLogicalId]; + // currently the CDK VPC only supports a single VPCCidrBlock per VPC so for now we won't allow multiple + // if we get requests for this we can update this to support multiple + if (vpcNode.vpcCidrBlockNode) { + throw new Error(`VPC ${vpcLogicalId} already has a VPCCidrBlock`); + } + vpcNode.vpcCidrBlockNode = node; + } + }); + } + for (const [construct, node] of this.constructNodes) { // No parent means this is the construct that represents the `Stack` if (construct.parent !== undefined) { @@ -228,7 +293,10 @@ export class GraphBuilder { sort(node); } - return sorted; + return { + nodes: sorted, + vpcNodes: this.vpcNodes, + }; } private addEdgesForCfnResource(obj: any, source: GraphNode): void { @@ -285,9 +353,25 @@ export class GraphBuilder { private addEdgesForIntrinsic(fn: string, params: any, source: GraphNode) { switch (fn) { - case 'Fn::GetAtt': - this.addEdgeForRef(params[0], source); + case 'Fn::GetAtt': { + let logicalId = params[0]; + const attributeName = params[1]; + // Special case for VPC Ipv6CidrBlocks + // Ipv6 cidr blocks are added to the VPC through a separate VpcCidrBlock resource + // Due to [pulumi/pulumi-aws-native#1798] the `Ipv6CidrBlocks` attribute will always be empty + // and we need to instead pull the `Ipv6CidrBlock` attribute from the VpcCidrBlock resource. + // Here we switching the dependency to be on the `VpcCidrBlock` resource (since that will also have a dependency + // on the VPC resource) + if ( + logicalId in this.vpcNodes && + attributeName === 'Ipv6CidrBlocks' && + this.vpcNodes[logicalId].vpcCidrBlockNode?.logicalId + ) { + logicalId = this.vpcNodes[logicalId].vpcCidrBlockNode!.logicalId; + } + this.addEdgeForRef(logicalId, source); break; + } case 'Fn::Sub': { const [template, vars] = diff --git a/tests/converters/app-converter.test.ts b/tests/converters/app-converter.test.ts index e76c8bbf..f59dce31 100644 --- a/tests/converters/app-converter.test.ts +++ b/tests/converters/app-converter.test.ts @@ -1,6 +1,7 @@ import { AppConverter, StackConverter } from '../../src/converters/app-converter'; +import * as native from '@pulumi/aws-native'; import { Stack } from 'aws-cdk-lib/core'; -import { AppComponent, AppOptions, PulumiStack } 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'; @@ -8,11 +9,13 @@ import { BucketPolicy } from '@pulumi/aws-native/s3'; import { createStackManifest } from '../utils'; import { promiseOf, setMocks } from '../mocks'; import { CdkConstruct } from '../../src/interop'; +import { StackManifest } from '../../src/assembly'; +import { MockResourceArgs } from '@pulumi/pulumi/runtime'; class MockAppComponent extends pulumi.ComponentResource implements AppComponent { public readonly name = 'stack'; public readonly assemblyDir: string; - stacks: { [artifactId: string]: PulumiStack } = {}; + stacks: { [artifactId: string]: Stack } = {}; dependencies: CdkConstruct[] = []; component: pulumi.ComponentResource; @@ -25,8 +28,10 @@ class MockAppComponent extends pulumi.ComponentResource implements AppComponent } } +let resources: MockResourceArgs[] = []; beforeAll(() => { - setMocks(); + resources = []; + setMocks(resources); }); describe('App Converter', () => { @@ -267,6 +272,192 @@ describe('App Converter', () => { ); }); +describe('Stack Converter', () => { + test('can convert', async () => { + const manifest = new StackManifest({ + id: 'stack', + templatePath: 'test/stack', + metadata: { + 'stack/vpc': 'vpc', + 'stack/cidr': 'cidr', + 'stack/other': 'other', + }, + tree: { + path: 'stack', + id: 'stack', + children: { + vpc: { + id: 'vpc', + path: 'stack/vpc', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPC', + }, + }, + cidr: { + id: 'cidr', + path: 'stack/cidr', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPCCidrBlock', + }, + }, + other: { + id: 'other', + path: 'stack/other', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::Subnet', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.Stack', + version: '2.149.0', + }, + }, + template: { + Resources: { + vpc: { + Type: 'AWS::EC2::VPC', + Properties: {}, + }, + cidr: { + Type: 'AWS::EC2::VPCCidrBlock', + Properties: { + VpcId: { Ref: 'vpc' }, + Ipv6CidrBlock: 'cidr_ipv6AddressAttribute', + }, + }, + other: { + Type: 'AWS::EC2::Subnet', + Properties: { + Ipv6CidrBlock: { 'Fn::Select': [0, { 'Fn::GetAtt': ['vpc', 'Ipv6CidrBlocks'] }] }, + }, + }, + }, + }, + dependencies: [], + }); + const converter = new StackConverter(new MockAppComponent('/tmp/foo/bar/does/not/exist'), manifest); + converter.convert(new Set()); + const subnet = converter.resources.get('other')?.resource as native.ec2.Subnet; + const cidrBlock = await promiseOf(subnet.ipv6CidrBlock); + expect(cidrBlock).toEqual('cidr_ipv6AddressAttribute'); + }); + + test('can convert multiple', async () => { + const manifest = new StackManifest({ + id: 'stack', + templatePath: 'test/stack', + metadata: { + 'stack/vpc': 'vpc', + 'stack/cidr': 'cidr', + 'stack/other': 'other', + 'stack/vpc2': 'vpc2', + 'stack/cidr2': 'cidr2', + 'stack/other2': 'other2', + }, + tree: { + path: 'stack', + id: 'stack', + children: { + vpc: { + id: 'vpc', + path: 'stack/vpc', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPC', + }, + }, + cidr: { + id: 'cidr', + path: 'stack/cidr', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPCCidrBlock', + }, + }, + other: { + id: 'other', + path: 'stack/other', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::Subnet', + }, + }, + vpc2: { + id: 'vpc2', + path: 'stack/vpc2', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPC', + }, + }, + cidr2: { + id: 'cidr2', + path: 'stack/cidr2', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPCCidrBlock', + }, + }, + other2: { + id: 'other2', + path: 'stack/other2', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::Subnet', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.Stack', + version: '2.149.0', + }, + }, + template: { + Resources: { + vpc: { + Type: 'AWS::EC2::VPC', + Properties: {}, + }, + cidr: { + Type: 'AWS::EC2::VPCCidrBlock', + Properties: { + VpcId: { Ref: 'vpc' }, + Ipv6CidrBlock: 'cidr_ipv6AddressAttribute', + }, + }, + other: { + Type: 'AWS::EC2::Subnet', + Properties: { + Ipv6CidrBlock: { 'Fn::Select': [0, { 'Fn::GetAtt': ['vpc', 'Ipv6CidrBlocks'] }] }, + }, + }, + vpc2: { + Type: 'AWS::EC2::VPC', + Properties: {}, + }, + cidr2: { + Type: 'AWS::EC2::VPCCidrBlock', + Properties: { + VpcId: { Ref: 'vpc2' }, + Ipv6CidrBlock: 'cidr_ipv6AddressAttribute_2', + }, + }, + other2: { + Type: 'AWS::EC2::Subnet', + Properties: { + Ipv6CidrBlock: { 'Fn::Select': [0, { 'Fn::GetAtt': ['vpc2', 'Ipv6CidrBlocks'] }] }, + }, + }, + }, + }, + dependencies: [], + }); + const converter = new StackConverter(new MockAppComponent('/tmp/foo/bar/does/not/exist'), manifest); + converter.convert(new Set()); + const subnet = converter.resources.get('other')?.resource as native.ec2.Subnet; + const cidrBlock = await promiseOf(subnet.ipv6CidrBlock); + expect(cidrBlock).toEqual('cidr_ipv6AddressAttribute'); + const subnet2 = converter.resources.get('other2')?.resource as native.ec2.Subnet; + const cidrBlock2 = await promiseOf(subnet2.ipv6CidrBlock); + expect(cidrBlock2).toEqual('cidr_ipv6AddressAttribute_2'); + }); +}); + function createUrn(resource: string, logicalId: string): string { return `urn:pulumi:stack::project::cdk:construct:aws-cdk-lib/aws_s3:${resource}$aws-native:s3:${resource}::${logicalId}`; } diff --git a/tests/graph.test.ts b/tests/graph.test.ts index 42c72590..199b09c6 100644 --- a/tests/graph.test.ts +++ b/tests/graph.test.ts @@ -188,7 +188,7 @@ describe('GraphBuilder', () => { }, ], ])('Parses the graph correctly', (graph, path, expected) => { - const actual = graph.find((node) => node.construct.path === path); + const actual = graph.nodes.find((node) => node.construct.path === path); expect(actual).toBeDefined(); expect(actual!.logicalId).toEqual(expected.logicalId); expect(actual!.resource).toEqual(expected.resource); @@ -225,7 +225,7 @@ describe('GraphBuilder', () => { }), ], ])('adds edge for %s', (_name, stackManifest) => { - const graph = GraphBuilder.build(stackManifest); + const graph = GraphBuilder.build(stackManifest).nodes; expect(graph[1].construct.path).toEqual('stack/resource-1'); expect(edgesToArray(graph[1].incomingEdges)).toEqual(['stack/resource-2']); expect(edgesToArray(graph[1].outgoingEdges)).toEqual(['stack']); @@ -235,6 +235,163 @@ describe('GraphBuilder', () => { }); }); +test('vpc with ipv6 cidr block', () => { + const nodes = GraphBuilder.build( + new StackManifest({ + id: 'stack', + templatePath: 'test/stack', + metadata: { + 'stack/vpc': 'vpc', + 'stack/cidr': 'cidr', + 'stack/other': 'other', + }, + tree: { + path: 'stack', + id: 'stack', + children: { + vpc: { + id: 'vpc', + path: 'stack/vpc', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPC', + }, + }, + cidr: { + id: 'cidr', + path: 'stack/cidr', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPCCidrBlock', + }, + }, + other: { + id: 'other', + path: 'stack/other', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::Other::Resource', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.Stack', + version: '2.149.0', + }, + }, + template: { + Resources: { + vpc: { + Type: 'AWS::EC2::VPC', + Properties: {}, + }, + cidr: { + Type: 'AWS::EC2::VPCCidrBlock', + Properties: { + VpcId: { Ref: 'vpc' }, + }, + }, + other: { + Type: 'AWS::Other::Resource', + Properties: { + SomeProp: { 'Fn::Select': [0, { 'Fn::GetAtt': ['vpc', 'Ipv6CidrBlocks'] }] }, + }, + }, + }, + }, + dependencies: [], + }), + ).nodes; + expect(nodes[0].construct.type).toEqual('aws-cdk-lib:Stack'); + expect(nodes[1].construct.type).toEqual('VPC'); + expect(nodes[2].construct.type).toEqual('VPCCidrBlock'); + expect(nodes[2].incomingEdges.size).toEqual(1); + expect(nodes[3].construct.type).toEqual('Resource'); + + // The other resource should have it's edge swapped to the cidr resource + expect(Array.from(nodes[2].incomingEdges.values())[0].logicalId).toEqual('other'); + expect(Array.from(nodes[3].outgoingEdges.values())[1].logicalId).toEqual('cidr'); +}); + +test('vpc with multiple ipv6 cidr blocks fails', () => { + expect(() => { + GraphBuilder.build( + new StackManifest({ + id: 'stack', + templatePath: 'test/stack', + metadata: { + 'stack/vpc': 'vpc', + 'stack/cidr': 'cidr', + 'stack/cidr2': 'cidr2', + 'stack/other': 'other', + }, + tree: { + path: 'stack', + id: 'stack', + children: { + vpc: { + id: 'vpc', + path: 'stack/vpc', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPC', + }, + }, + cidr: { + id: 'cidr', + path: 'stack/cidr', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPCCidrBlock', + }, + }, + cidr2: { + id: 'cidr2', + path: 'stack/cidr2', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::EC2::VPCCidrBlock', + }, + }, + other: { + id: 'other', + path: 'stack/other', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::Other::Resource', + }, + }, + }, + constructInfo: { + fqn: 'aws-cdk-lib.Stack', + version: '2.149.0', + }, + }, + template: { + Resources: { + vpc: { + Type: 'AWS::EC2::VPC', + Properties: {}, + }, + cidr: { + Type: 'AWS::EC2::VPCCidrBlock', + Properties: { + VpcId: { Ref: 'vpc' }, + }, + }, + cidr2: { + Type: 'AWS::EC2::VPCCidrBlock', + Properties: { + VpcId: { Ref: 'vpc' }, + }, + }, + other: { + Type: 'AWS::Other::Resource', + Properties: { + SomeProp: { 'Fn::Select': [0, { 'Fn::GetAtt': ['vpc', 'Ipv6CidrBlocks'] }] }, + }, + }, + }, + }, + dependencies: [], + }), + ).nodes; + }).toThrow(/VPC vpc already has a VPCCidrBlock/); +}); + test('pulumi resource type name fallsback when fqn not available', () => { const bucketId = 'example-bucket'; const policyResourceId = 'Policy'; @@ -308,7 +465,7 @@ test('pulumi resource type name fallsback when fqn not available', () => { }, dependencies: [], }), - ); + ).nodes; expect(nodes[0].construct.type).toEqual('aws-cdk-lib:Stack'); expect(nodes[1].construct.type).toEqual(bucketId); diff --git a/yarn.lock b/yarn.lock index 9fbaa233..92dbb49b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1028,10 +1028,10 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== -"@pulumi/aws-native@^1.0.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@pulumi/aws-native/-/aws-native-1.3.0.tgz#10f654daa1cc578ab78a25ef614f888dd23e3276" - integrity sha512-egocWUmAmrRk+/LWof3yWdn+qrLy9rHUmrg5XjRP1SUo7pQgqEYMKY6IlV/81NV2zcdk6t65YOmumOsTFFoMuQ== +"@pulumi/aws-native@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@pulumi/aws-native/-/aws-native-1.6.0.tgz#8947829fb7fdcef8ce62fe43082b668068281101" + integrity sha512-7b6CVr8XoprdQCWCnmPDhHe+BGHN2LSk+qpeELp8M2YjcwEu2XkFhI7bAj2UjQLcD84HwPM07baM5f2uryrZxA== dependencies: "@pulumi/pulumi" "^3.136.0"