Skip to content

Commit

Permalink
Fix cidr intrinsic (#1791)
Browse files Browse the repository at this point in the history
The `cidr` intrinsic function has been broken. This PR fixes the cidr
function and adds some tests. Took some inspiration from [OpenTofu
cidrsubnets](https://github.com/opentofu/opentofu/blob/9d842aa920e16cc3275f4fe54240634fff5146dc/internal/lang/funcs/cidr.go#L123)

fixes #1566, fixes #593
  • Loading branch information
corymhall authored Nov 1, 2024
1 parent aa47ae6 commit 7739cf2
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 10 deletions.
41 changes: 31 additions & 10 deletions provider/pkg/provider/provider_intrinsics.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (p *cfnProvider) getAZs(ctx context.Context, inputs resource.PropertyMap) (
}), nil
}

func (p *cfnProvider) cidr(ctx context.Context, inputs resource.PropertyMap) (resource.PropertyMap, error) {
func cidr(inputs resource.PropertyMap) (resource.PropertyMap, error) {
ipBlock, ok := inputs["ipBlock"]
if !ok {
return nil, fmt.Errorf("missing required property 'ipBlock'")
Expand All @@ -53,15 +53,15 @@ func (p *cfnProvider) cidr(ctx context.Context, inputs resource.PropertyMap) (re

count, ok := inputs["count"]
if !ok {
return nil, fmt.Errorf("mising required property 'count'")
return nil, fmt.Errorf("missing required property 'count'")
}
if !count.IsNumber() || count.NumberValue() < 1 || count.NumberValue() > 256 {
return nil, fmt.Errorf("'count' must be a number between 1 and 256")
}

cidrBits, ok := inputs["cidrBits"]
if !ok {
return nil, fmt.Errorf("mising required property 'cidrBits'")
return nil, fmt.Errorf("missing required property 'cidrBits'")
}
if !cidrBits.IsNumber() || cidrBits.NumberValue() < 0 {
return nil, fmt.Errorf("'cidrBits' must be a positive number")
Expand All @@ -73,22 +73,43 @@ func (p *cfnProvider) cidr(ctx context.Context, inputs resource.PropertyMap) (re
}

subnets := make([]resource.PropertyValue, int(count.NumberValue()))
subnets[0] = resource.NewStringProperty(network.String())
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)
}

prefixLen := int(cidrBits.NumberValue())
for i := 1; i < len(subnets)-1; i++ {
subnet, ok := gocidr.NextSubnet(network, prefixLen)
if !ok {
return nil, fmt.Errorf("could not create %d subnets", len(subnets))
current, ok := gocidr.PreviousSubnet(network, prefixLen)
// ok is true if we have rolled over (which we don't want)
if ok {
return nil, fmt.Errorf("not enough remaining address space for a subnet with a prefix of %d bits after %s", len(subnets), current.String())
}
for i := 0; i < len(subnets); i++ {
subnet, ok := gocidr.NextSubnet(current, prefixLen)
if ok || !network.Contains(subnet.IP) {
return nil, fmt.Errorf("not enough remaining address space for a subnet with a prefix of %d bits after %s", len(subnets), current.String())
}
subnets[i], network = resource.NewStringProperty(subnet.String()), subnet
current = subnet
subnets[i] = resource.NewStringProperty(subnet.String())
}

return resource.PropertyMap{
"subnets": resource.NewArrayProperty(subnets),
}, nil
}

func (p *cfnProvider) cidr(ctx context.Context, inputs resource.PropertyMap) (resource.PropertyMap, error) {
return cidr(inputs)
}

func (p *cfnProvider) importValue(ctx context.Context, inputs resource.PropertyMap) (resource.PropertyMap, error) {
name, ok := inputs["name"]
if !ok {
Expand Down
88 changes: 88 additions & 0 deletions provider/pkg/provider/provider_intrinsics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package provider

import (
"testing"

"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/stretchr/testify/assert"
)

func Test_cidr(t *testing.T) {
t.Run("ipv6, count=4, cidrbits=64", func(t *testing.T) {
res, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "2600:1f16:44e:3e00::/56",
"count": 4,
"cidrBits": 64,
}))
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"),
}, subnets)

})

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

})

t.Run("ipv4, count=1, cidrbits=60", func(t *testing.T) {
res, err := cidr(resource.NewPropertyMapFromMap(map[string]interface{}{
"ipBlock": "192.168.0.0/24",
"count": 6,
"cidrBits": 5,
}))
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"),
}, subnets)

})

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

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

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,
}))
assert.ErrorContains(t, err, "not enough remaining address space for a subnet with a prefix of 3 bits after 2600:1f16:44e:3e80::/57")
})

}

0 comments on commit 7739cf2

Please sign in to comment.