diff --git a/examples/cidr-ts/Pulumi.yaml b/examples/cidr-ts/Pulumi.yaml new file mode 100644 index 0000000000..05ff22ee13 --- /dev/null +++ b/examples/cidr-ts/Pulumi.yaml @@ -0,0 +1,3 @@ +name: aws-cidr-ts +runtime: nodejs +description: A TypeScript Pulumi program with AWS Cloud Control provider diff --git a/examples/cidr-ts/index.ts b/examples/cidr-ts/index.ts new file mode 100644 index 0000000000..8619b46324 --- /dev/null +++ b/examples/cidr-ts/index.ts @@ -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]}); + } +}); diff --git a/examples/cidr-ts/package.json b/examples/cidr-ts/package.json new file mode 100644 index 0000000000..f4737cc950 --- /dev/null +++ b/examples/cidr-ts/package.json @@ -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" + } +} diff --git a/examples/cidr-ts/tsconfig.json b/examples/cidr-ts/tsconfig.json new file mode 100644 index 0000000000..ab65afa613 --- /dev/null +++ b/examples/cidr-ts/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" + ] +} diff --git a/examples/examples_nodejs_test.go b/examples/examples_nodejs_test.go index aaa69d8b35..fc353dc088 100644 --- a/examples/examples_nodejs_test.go +++ b/examples/examples_nodejs_test.go @@ -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{ diff --git a/provider/pkg/provider/provider_intrinsics.go b/provider/pkg/provider/provider_intrinsics.go index e5a2950ee0..0ef3a3bf2b 100644 --- a/provider/pkg/provider/provider_intrinsics.go +++ b/provider/pkg/provider/provider_intrinsics.go @@ -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 { @@ -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 { diff --git a/provider/pkg/provider/provider_intrinsics_test.go b/provider/pkg/provider/provider_intrinsics_test.go index 13566ee191..d44030fb27 100644 --- a/provider/pkg/provider/provider_intrinsics_test.go +++ b/provider/pkg/provider/provider_intrinsics_test.go @@ -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, @@ -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") }) }