Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix IPV6 cidr blocks #197

Merged
merged 4 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions integration/ec2/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: pulumi-aws-ec2
runtime: nodejs
description: ec2 integration test
127 changes: 127 additions & 0 deletions integration/ec2/index.ts
Original file line number Diff line number Diff line change
@@ -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;
},
},
},
);
15 changes: 15 additions & 0 deletions integration/ec2/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
18 changes: 18 additions & 0 deletions integration/ec2/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
9 changes: 9 additions & 0 deletions integration/examples_nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 22 additions & 6 deletions src/converters/app-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
private readonly cdkStack: cdk.Stack;

private _stackResource?: CdkConstruct;
private _graph: GraphBuilder;

public get stackResource(): CdkConstruct {
if (!this._stackResource) {
Expand All @@ -105,10 +106,11 @@
constructor(host: AppComponent, readonly stack: StackManifest) {
super(host);
this.cdkStack = host.stacks[stack.id];
this._graph = new GraphBuilder(stack);
}

public convert(dependencies: Set<ArtifactConverter>) {
const dependencyGraphNodes = GraphBuilder.build(this.stack);
const dependencyGraphNodes = this._graph.build();

// process parameters first because resources will reference them
for (const [logicalId, value] of Object.entries(this.stack.parameters ?? {})) {
Expand Down Expand Up @@ -344,7 +346,7 @@
}

return Object.entries(obj)
.filter(([_, v]) => !this.isNoValue(v))

Check warning on line 349 in src/converters/app-converter.ts

View workflow job for this annotation

GitHub Actions / Run lint

'_' is defined but never used
.reduce((result, [k, v]) => ({ ...result, [k]: this.processIntrinsics(v) }), {});
}

Expand All @@ -359,7 +361,21 @@
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 === this._graph.vpcNode?.logicalId &&
attributeName === 'Ipv6CidrBlocks' &&
this._graph.vpcCidrBlockNode?.logicalId
) {
return [this.resolveAtt(this._graph.vpcCidrBlockNode.logicalId, 'Ipv6CidrBlock')];
}

return this.resolveAtt(params[0], params[1]);
}

Expand All @@ -375,23 +391,23 @@
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));

case 'Fn::Sub':
return lift((params) => {
const [template, vars] =

Check warning on line 410 in src/converters/app-converter.ts

View workflow job for this annotation

GitHub Actions / Run lint

'vars' is assigned a value but never used
typeof params === 'string' ? [params, undefined] : [params[0] as string, params[1]];

const parts: string[] = [];
Expand Down
36 changes: 31 additions & 5 deletions src/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,19 @@
// Map of resource logicalId to GraphNode. Allows for easy lookup by logicalId
cfnElementNodes: Map<string, GraphNode>;

// If the app has a VpcCidrBlock resource, this will be set to the GraphNode representing it
vpcCidrBlockNode?: GraphNode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd prefer if we'd save that in the result of GraphBuilder::build. E.g. turn this into a GraphResult struct that has the necessary params we need.
I wouldn't expect a Builder to store state beyond what's needed to build

// If the app has a Vpc resource, this will be set to the GraphNode representing it
vpcNode?: GraphNode;

constructor(private readonly stack: StackManifest) {
this.constructNodes = new Map<ConstructInfo, GraphNode>();
this.cfnElementNodes = new Map<string, GraphNode>();
}

// 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 build(): GraphNode[] {
return this._build();
}

/**
Expand Down Expand Up @@ -163,6 +167,12 @@
`Something went wrong: resourceType ${resource.Type} does not equal CfnType ${cfnType}`,
);
}
if (resource.Type === 'AWS::EC2::VPCCidrBlock') {
this.vpcCidrBlockNode = node;
}
if (resource.Type === 'AWS::EC2::VPC') {
this.vpcNode = node;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a stack only have a single VPC?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that case would be exceedingly rare, but I should be able to update this to handle that case

}
}
this.constructNodes.set(construct, node);
if (tree.children) {
Expand Down Expand Up @@ -224,7 +234,7 @@
sorted.push(node);
}

for (const [_, node] of this.constructNodes) {

Check warning on line 237 in src/graph.ts

View workflow job for this annotation

GitHub Actions / Run lint

'_' is assigned a value but never used
sort(node);
}

Expand Down Expand Up @@ -285,9 +295,25 @@

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 === this.vpcNode?.logicalId &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along the lines of my earlier Q, what happens if the stack has multiple VPCs?

attributeName === 'Ipv6CidrBlocks' &&
this.vpcCidrBlockNode?.logicalId
) {
logicalId = this.vpcCidrBlockNode.logicalId;
}
this.addEdgeForRef(logicalId, source);
break;
}
case 'Fn::Sub':
{
const [template, vars] =
Expand Down
Loading
Loading