Various interpolation functions for CIDR range manipulation.
These new functions allow Terraform to be used for network address space planning tasks, and make it easier to produce reusable modules that contain or depend on network infrastructure. For example: - cidrsubnet allows an aws_subnet to derive its CIDR prefix from its parent aws_vpc. - cidrhost allows a fixed IP address for a resource to be assigned within an address range defined elsewhere. - cidrnetmask provides the dotted-decimal form of a prefix length that is accepted by some systems such as routing tables and static network interface configuration files. The bulk of the work here is done by an external library I authored called go-cidr. It is MIT licensed and was implemented primarily for the purpose of using it within Terraform. It has its own unit tests and so the unit tests within this change focus on simple success cases and on the correct handling of the various error cases.
This commit is contained in:
parent
53b64909ec
commit
ef161e1c1b
|
@ -6,11 +6,13 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/apparentlymart/go-cidr/cidr"
|
||||
"github.com/hashicorp/terraform/config/lang/ast"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
@ -20,6 +22,9 @@ var Funcs map[string]ast.Function
|
|||
|
||||
func init() {
|
||||
Funcs = map[string]ast.Function{
|
||||
"cidrhost": interpolationFuncCidrHost(),
|
||||
"cidrnetmask": interpolationFuncCidrNetmask(),
|
||||
"cidrsubnet": interpolationFuncCidrSubnet(),
|
||||
"compact": interpolationFuncCompact(),
|
||||
"concat": interpolationFuncConcat(),
|
||||
"element": interpolationFuncElement(),
|
||||
|
@ -54,6 +59,92 @@ func interpolationFuncCompact() ast.Function {
|
|||
}
|
||||
}
|
||||
|
||||
// interpolationFuncCidrHost implements the "cidrhost" function that
|
||||
// fills in the host part of a CIDR range address to create a single
|
||||
// host address
|
||||
func interpolationFuncCidrHost() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{
|
||||
ast.TypeString, // starting CIDR mask
|
||||
ast.TypeInt, // host number to insert
|
||||
},
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: false,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
hostNum := args[1].(int)
|
||||
_, network, err := net.ParseCIDR(args[0].(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid CIDR expression: %s", err)
|
||||
}
|
||||
|
||||
ip, err := cidr.Host(network, hostNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ip.String(), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// interpolationFuncCidrNetmask implements the "cidrnetmask" function
|
||||
// that returns the subnet mask in IP address notation.
|
||||
func interpolationFuncCidrNetmask() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{
|
||||
ast.TypeString, // CIDR mask
|
||||
},
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: false,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
_, network, err := net.ParseCIDR(args[0].(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid CIDR expression: %s", err)
|
||||
}
|
||||
|
||||
return net.IP(network.Mask).String(), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// interpolationFuncCidrSubnet implements the "cidrsubnet" function that
|
||||
// adds an additional subnet of the given length onto an existing
|
||||
// IP block expressed in CIDR notation.
|
||||
func interpolationFuncCidrSubnet() ast.Function {
|
||||
return ast.Function{
|
||||
ArgTypes: []ast.Type{
|
||||
ast.TypeString, // starting CIDR mask
|
||||
ast.TypeInt, // number of bits to extend the prefix
|
||||
ast.TypeInt, // network number to append to the prefix
|
||||
},
|
||||
ReturnType: ast.TypeString,
|
||||
Variadic: false,
|
||||
Callback: func(args []interface{}) (interface{}, error) {
|
||||
extraBits := args[1].(int)
|
||||
subnetNum := args[2].(int)
|
||||
_, network, err := net.ParseCIDR(args[0].(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid CIDR expression: %s", err)
|
||||
}
|
||||
|
||||
// For portability with 32-bit systems where the subnet number
|
||||
// will be a 32-bit int, we only allow extension of 32 bits in
|
||||
// one call even if we're running on a 64-bit machine.
|
||||
// (Of course, this is significant only for IPv6.)
|
||||
if extraBits > 32 {
|
||||
return nil, fmt.Errorf("may not extend prefix by more than 32 bits")
|
||||
}
|
||||
|
||||
newNetwork, err := cidr.Subnet(network, extraBits, subnetNum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newNetwork.String(), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// interpolationFuncConcat implements the "concat" function that
|
||||
// concatenates multiple strings. This isn't actually necessary anymore
|
||||
// since our language supports string concat natively, but for backwards
|
||||
|
|
|
@ -38,6 +38,115 @@ func TestInterpolateFuncCompact(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestInterpolateFuncCidrHost(t *testing.T) {
|
||||
testFunction(t, testFunctionConfig{
|
||||
Cases: []testFunctionCase{
|
||||
{
|
||||
`${cidrhost("192.168.1.0/24", 5)}`,
|
||||
"192.168.1.5",
|
||||
false,
|
||||
},
|
||||
{
|
||||
`${cidrhost("192.168.1.0/30", 255)}`,
|
||||
nil,
|
||||
true, // 255 doesn't fit in two bits
|
||||
},
|
||||
{
|
||||
`${cidrhost("not-a-cidr", 6)}`,
|
||||
nil,
|
||||
true, // not a valid CIDR mask
|
||||
},
|
||||
{
|
||||
`${cidrhost("10.256.0.0/8", 6)}`,
|
||||
nil,
|
||||
true, // can't have an octet >255
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterpolateFuncCidrNetmask(t *testing.T) {
|
||||
testFunction(t, testFunctionConfig{
|
||||
Cases: []testFunctionCase{
|
||||
{
|
||||
`${cidrnetmask("192.168.1.0/24")}`,
|
||||
"255.255.255.0",
|
||||
false,
|
||||
},
|
||||
{
|
||||
`${cidrnetmask("192.168.1.0/32")}`,
|
||||
"255.255.255.255",
|
||||
false,
|
||||
},
|
||||
{
|
||||
`${cidrnetmask("0.0.0.0/0")}`,
|
||||
"0.0.0.0",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// This doesn't really make sense for IPv6 networks
|
||||
// but it ought to do something sensible anyway.
|
||||
`${cidrnetmask("1::/64")}`,
|
||||
"ffff:ffff:ffff:ffff::",
|
||||
false,
|
||||
},
|
||||
{
|
||||
`${cidrnetmask("not-a-cidr")}`,
|
||||
nil,
|
||||
true, // not a valid CIDR mask
|
||||
},
|
||||
{
|
||||
`${cidrnetmask("10.256.0.0/8")}`,
|
||||
nil,
|
||||
true, // can't have an octet >255
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterpolateFuncCidrSubnet(t *testing.T) {
|
||||
testFunction(t, testFunctionConfig{
|
||||
Cases: []testFunctionCase{
|
||||
{
|
||||
`${cidrsubnet("192.168.2.0/20", 4, 6)}`,
|
||||
"192.168.6.0/24",
|
||||
false,
|
||||
},
|
||||
{
|
||||
`${cidrsubnet("fe80::/48", 16, 6)}`,
|
||||
"fe80:0:0:6::/64",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// IPv4 address encoded in IPv6 syntax gets normalized
|
||||
`${cidrsubnet("::ffff:192.168.0.0/112", 8, 6)}`,
|
||||
"192.168.6.0/24",
|
||||
false,
|
||||
},
|
||||
{
|
||||
`${cidrsubnet("192.168.0.0/30", 4, 6)}`,
|
||||
nil,
|
||||
true, // not enough bits left
|
||||
},
|
||||
{
|
||||
`${cidrsubnet("192.168.0.0/16", 2, 16)}`,
|
||||
nil,
|
||||
true, // can't encode 16 in 2 bits
|
||||
},
|
||||
{
|
||||
`${cidrsubnet("not-a-cidr", 4, 6)}`,
|
||||
nil,
|
||||
true, // not a valid CIDR mask
|
||||
},
|
||||
{
|
||||
`${cidrsubnet("10.256.0.0/8", 4, 6)}`,
|
||||
nil,
|
||||
true, // can't have an octet >255
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterpolateFuncDeprecatedConcat(t *testing.T) {
|
||||
testFunction(t, testFunctionConfig{
|
||||
Cases: []testFunctionCase{
|
||||
|
|
|
@ -80,6 +80,22 @@ The supported built-in functions are:
|
|||
* `base64encode(string)` - Returns a base64-encoded representation of the
|
||||
given string.
|
||||
|
||||
* `cidrhost(iprange, hostnum)` - Takes an IP address range in CIDR notation
|
||||
and creates an IP address with the given host number. For example,
|
||||
``cidrhost("10.0.0.0/8", 2)`` returns ``10.0.0.2``.
|
||||
|
||||
* `cidrnetmask(iprange)` - Takes an IP address range in CIDR notation
|
||||
and returns the address-formatted subnet mask format that some
|
||||
systems expect for IPv4 interfaces. For example,
|
||||
``cidrmask("10.0.0.0/8")`` returns ``255.0.0.0``. Not applicable
|
||||
to IPv6 networks since CIDR notation is the only valid notation for
|
||||
IPv6.
|
||||
|
||||
* `cidrsubnet(iprange, newbits, netnum)` - Takes an IP address range in
|
||||
CIDR notation (like ``10.0.0.0/8``) and extends its prefix to include an
|
||||
additional subnet number. For example,
|
||||
``cidrsubnet("10.0.0.0/8", 8, 2)`` returns ``10.2.0.0/16``.
|
||||
|
||||
* `compact(list)` - Removes empty string elements from a list. This can be
|
||||
useful in some cases, for example when passing joined lists as module
|
||||
variables or when parsing module outputs.
|
||||
|
|
Loading…
Reference in New Issue