diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ac202b3..4b12902f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ FEATURES: * **New resources: `aws_codeploy_app` and `aws_codeploy_deployment_group`** [GH-2783] * New remote state backend: `etcd` [GH-3487] * New interpolation functions: `upper` and `lower` [GH-3558] + * New interpolation functions: `cidrhost`, `cidrnetmask` and `cidrsubnet` [GH-3127] BUG FIXES: diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go index 1b58ac93c..e98ade2f0 100644 --- a/config/interpolate_funcs.go +++ b/config/interpolate_funcs.go @@ -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 diff --git a/config/interpolate_funcs_test.go b/config/interpolate_funcs_test.go index f40f56860..7b7311fd4 100644 --- a/config/interpolate_funcs_test.go +++ b/config/interpolate_funcs_test.go @@ -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{ diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 940839076..049c71825 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -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.