Skip to content

Commit

Permalink
Fix cidr function (#1797)
Browse files Browse the repository at this point in the history
In #1791 we \"fixed\" the cidr function so that it would generate
subnets
correctly. We did not fix it correctly though. This cidr function needs
to match the logic that the CloudFormation Fn::Cidr function has.

Critically the `cidrBits` logic was incorrect. It should be the inverse
of the subnet mask that you want to have for the subnets.

For example a `cidrBits` of 5 means that I want subnets with a mask of
`/27` (32-5=27).

```ts
cidr({
  ipBlock: '192.168.0.0/24',
  count: 6,
  cidrBits: 5,
});
```
  • Loading branch information
corymhall authored Nov 4, 2024
1 parent 0166a63 commit 895bbfc
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 40 deletions.
3 changes: 3 additions & 0 deletions examples/cidr-ts/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: aws-cidr-ts
runtime: nodejs
description: A TypeScript Pulumi program with AWS Cloud Control provider
48 changes: 48 additions & 0 deletions examples/cidr-ts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2016-2024, Pulumi Corporation.

import * as pulumi from '@pulumi/pulumi';
import * as aws from "@pulumi/aws-native";

const cidrBlock = '192.168.0.0/24';
const vpc = new aws.ec2.Vpc('vpc', {
cidrBlock,
});

const ipv6Cidr = new aws.ec2.VpcCidrBlock('ipv6Cidr', {
vpcId: vpc.vpcId,
amazonProvidedIpv6CidrBlock: true,
});


const cidrs = aws.cidrOutput({
count: 4,
ipBlock: cidrBlock,
cidrBits: 5
});
const ipvcCidrs = aws.cidrOutput({
ipBlock: ipv6Cidr.ipv6CidrBlock.apply(cidr => cidr!),
cidrBits: 64,
count: 4,
});

ipvcCidrs.apply(cidr => {
if (cidr.subnets.length !== 4) {
throw new Error('Expected 4 ipv6 CIDRs');
}
})

cidrs.apply(cidr => {
if (cidr.subnets.length !== 4) {
throw new Error('Expected 4 ipv4 CIDRs');
}
})

pulumi.all([cidrs.subnets, ipvcCidrs.subnets]).apply(([ipv4, ipv6]) => {
for (let i = 0; i < 4; i++) {
new aws.ec2.Subnet(`subnet-${i}`, {
vpcId: vpc.id,
cidrBlock: ipv4[i],
ipv6CidrBlock: ipv6[i]
}, { dependsOn: [ipv6Cidr]});
}
});
12 changes: 12 additions & 0 deletions examples/cidr-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "aws-cidr-ts",
"devDependencies": {
"@types/node": "^8.0.0"
},
"dependencies": {
"@pulumi/pulumi": "^3.136.0"
},
"peerDependencies": {
"@pulumi/aws-native": "dev"
}
}
18 changes: 18 additions & 0 deletions examples/cidr-ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
9 changes: 9 additions & 0 deletions examples/examples_nodejs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ func TestGetTs(t *testing.T) {
integration.ProgramTest(t, &test)
}

func TestVpcCidrs(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "cidr-ts"),
})

integration.ProgramTest(t, &test)
}

func TestUpdate(t *testing.T) {
test := getJSBaseOptions(t).
With(integration.ProgramTestOptions{
Expand Down
35 changes: 23 additions & 12 deletions provider/pkg/provider/provider_intrinsics.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ func (p *cfnProvider) getAZs(ctx context.Context, inputs resource.PropertyMap) (
}), nil
}

// Goal is to implement the Fn::Cidr intrinsic function
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-cidr.html
// ipBlock: The user-specified CIDR block to be split into smaller CIDR blocks.
// count: The number of CIDR blocks to generate. Valid range is between 1 and 256.
// cidrBits: The number of subnet bits for the CIDR. For example, specifying a value "8" for this parameter will create a CIDR with a mask of "/24".
//
// Note: Subnet bits is the inverse of subnet mask. To calculate the required host bits
// for a given subnet bits, subtract the subnet bits from 32 for ipv4 or 128 for ipv6.
func cidr(inputs resource.PropertyMap) (resource.PropertyMap, error) {
ipBlock, ok := inputs["ipBlock"]
if !ok {
Expand Down Expand Up @@ -71,22 +79,25 @@ func cidr(inputs resource.PropertyMap) (resource.PropertyMap, error) {
if err != nil {
return nil, fmt.Errorf("invalid IP block: %s", err)
}
protocol := "IP"
bits := len(network.IP) * 8
switch bits {
case 32:
protocol = "IPv4"
case 128:
protocol = "IPv6"
}

subnets := make([]resource.PropertyValue, int(count.NumberValue()))
startPrefixLen, _ := network.Mask.Size()

prefixLen := int(cidrBits.NumberValue()) + startPrefixLen
if prefixLen > len(network.IP)*8 {
protocol := "IP"
switch len(network.IP) * 8 {
case 32:
protocol = "IPv4"
case 128:
protocol = "IPv6"
}
return nil, fmt.Errorf("cidrBits %d would extend prefix to %d bits, which is too long for an %s address", int(cidrBits.NumberValue()), prefixLen, protocol)
subnetBits := int(cidrBits.NumberValue())

if subnetBits > bits {
return nil, fmt.Errorf("cidrBits %d is more than %d bits for an %s address. \n"+
"cidrBits is the inverse of subnet mask, e.g. cidrBits=5 would create a subnet mask of '/27'", subnetBits, bits, protocol)
}

prefixLen := bits - subnetBits

current, ok := gocidr.PreviousSubnet(network, prefixLen)
// ok is true if we have rolled over (which we don't want)
if ok {
Expand Down
67 changes: 39 additions & 28 deletions provider/pkg/provider/provider_intrinsics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,29 @@ func Test_cidr(t *testing.T) {
assert.NoError(t, err)
subnets := res["subnets"].ArrayValue()
assert.Equal(t, []resource.PropertyValue{
resource.NewStringProperty("2600:1f16:44e:3e00::/120"),
resource.NewStringProperty("2600:1f16:44e:3e00::100/120"),
resource.NewStringProperty("2600:1f16:44e:3e00::200/120"),
resource.NewStringProperty("2600:1f16:44e:3e00::300/120"),
resource.NewStringProperty("2600:1f16:44e:3e00::/64"),
resource.NewStringProperty("2600:1f16:44e:3e01::/64"),
resource.NewStringProperty("2600:1f16:44e:3e02::/64"),
resource.NewStringProperty("2600:1f16:44e:3e03::/64"),
}, subnets)

})

t.Run("ipv6, count=1, cidrbits=60", func(t *testing.T) {
t.Run("ipv6, count=2, cidrbits=64", func(t *testing.T) {
res, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "2a05:d024:d::/56",
"count": 1,
"cidrBits": 4,
"ipBlock": "2600:1f16:19ee:8000::/56",
"count": 2,
"cidrBits": 64,
}))
assert.NoError(t, err)
subnets := res["subnets"].ArrayValue()
assert.Equal(t, []resource.PropertyValue{
resource.NewStringProperty("2a05:d024:d::/60"),
resource.NewStringProperty("2600:1f16:19ee:8000::/64"),
resource.NewStringProperty("2600:1f16:19ee:8001::/64"),
}, subnets)

})

t.Run("ipv4, count=1, cidrbits=60", func(t *testing.T) {
t.Run("ipv4, count=6, cidrbits=60", func(t *testing.T) {
res, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "192.168.0.0/24",
"count": 6,
Expand All @@ -48,41 +48,52 @@ func Test_cidr(t *testing.T) {
assert.NoError(t, err)
subnets := res["subnets"].ArrayValue()
assert.Equal(t, []resource.PropertyValue{
resource.NewStringProperty("192.168.0.0/29"),
resource.NewStringProperty("192.168.0.8/29"),
resource.NewStringProperty("192.168.0.16/29"),
resource.NewStringProperty("192.168.0.24/29"),
resource.NewStringProperty("192.168.0.32/29"),
resource.NewStringProperty("192.168.0.40/29"),
resource.NewStringProperty("192.168.0.0/27"),
resource.NewStringProperty("192.168.0.32/27"),
resource.NewStringProperty("192.168.0.64/27"),
resource.NewStringProperty("192.168.0.96/27"),
resource.NewStringProperty("192.168.0.128/27"),
resource.NewStringProperty("192.168.0.160/27"),
}, subnets)

})

t.Run("ipv4 too long prefix", func(t *testing.T) {
t.Run("ipv4 invalid cidrBits", func(t *testing.T) {
_, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "192.168.0.0/24",
"count": 6,
"cidrBits": 15,
"cidrBits": 36,
}))
assert.ErrorContains(t, err, "cidrBits 15 would extend prefix to 39 bits, which is too long for an IPv4 address")
assert.ErrorContains(t, err, "cidrBits 36 is more than 32 bits for an IPv4 address")
})

t.Run("ipv6 too long prefix", func(t *testing.T) {
t.Run("ipv6 invalid cidrBits", func(t *testing.T) {
_, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "2600:1f16:44e:3e00::/56",
"count": 6,
"cidrBits": 80,
"cidrBits": 129,
}))
assert.ErrorContains(t, err, "cidrBits 80 would extend prefix to 136 bits, which is too long for an IPv6 address")
assert.ErrorContains(t, err, "cidrBits 129 is more than 128 bits for an IPv6 address")
})

t.Run("ipv6 not enough space", func(t *testing.T) {
t.Run("ipv6, not enough space", func(t *testing.T) {
_, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "2600:1f16:44e:3e00::/56",
"count": 3,
"cidrBits": 1,
"ipBlock": "2a05:d024:d::/56",
"count": 3,
// 128 - 71 = 57 (2 subnets)
"cidrBits": 71,
}))
assert.ErrorContains(t, err, "not enough remaining address space for a subnet")
})

t.Run("ipv4, not enough space", func(t *testing.T) {
_, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "192.168.1.0/24",
"count": 5,
// 32 - 6 = 26 (4 subnets)
"cidrBits": 6,
}))
assert.ErrorContains(t, err, "not enough remaining address space for a subnet with a prefix of 3 bits after 2600:1f16:44e:3e80::/57")
assert.ErrorContains(t, err, "not enough remaining address space for a subnet")
})

}

0 comments on commit 895bbfc

Please sign in to comment.