Skip to content

Commit f66c6b4

Browse files
authored
Merge pull request #187 from hashicorp/cherry-pick-22858
lang/funcs: cidrsubnets function (cherry-pick hashicorp/terraform#22858)
2 parents 8b94610 + 4b33a1b commit f66c6b4

File tree

4 files changed

+206
-0
lines changed

4 files changed

+206
-0
lines changed

internal/lang/funcs/cidr.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,86 @@ var CidrSubnetFunc = function.New(&function.Spec{
113113
},
114114
})
115115

116+
// CidrSubnetsFunc is similar to CidrSubnetFunc but calculates many consecutive
117+
// subnet addresses at once, rather than just a single subnet extension.
118+
var CidrSubnetsFunc = function.New(&function.Spec{
119+
Params: []function.Parameter{
120+
{
121+
Name: "prefix",
122+
Type: cty.String,
123+
},
124+
},
125+
VarParam: &function.Parameter{
126+
Name: "newbits",
127+
Type: cty.Number,
128+
},
129+
Type: function.StaticReturnType(cty.List(cty.String)),
130+
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
131+
_, network, err := net.ParseCIDR(args[0].AsString())
132+
if err != nil {
133+
return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid CIDR expression: %s", err)
134+
}
135+
startPrefixLen, _ := network.Mask.Size()
136+
137+
prefixLengthArgs := args[1:]
138+
if len(prefixLengthArgs) == 0 {
139+
return cty.ListValEmpty(cty.String), nil
140+
}
141+
142+
var firstLength int
143+
if err := gocty.FromCtyValue(prefixLengthArgs[0], &firstLength); err != nil {
144+
return cty.UnknownVal(cty.String), function.NewArgError(1, err)
145+
}
146+
firstLength += startPrefixLen
147+
148+
retVals := make([]cty.Value, len(prefixLengthArgs))
149+
150+
current, _ := cidr.PreviousSubnet(network, firstLength)
151+
for i, lengthArg := range prefixLengthArgs {
152+
var length int
153+
if err := gocty.FromCtyValue(lengthArg, &length); err != nil {
154+
return cty.UnknownVal(cty.String), function.NewArgError(i+1, err)
155+
}
156+
157+
if length < 1 {
158+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "must extend prefix by at least one bit")
159+
}
160+
// For portability with 32-bit systems where the subnet number
161+
// will be a 32-bit int, we only allow extension of 32 bits in
162+
// one call even if we're running on a 64-bit machine.
163+
// (Of course, this is significant only for IPv6.)
164+
if length > 32 {
165+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "may not extend prefix by more than 32 bits")
166+
}
167+
length += startPrefixLen
168+
if length > (len(network.IP) * 8) {
169+
protocol := "IP"
170+
switch len(network.IP) * 8 {
171+
case 32:
172+
protocol = "IPv4"
173+
case 128:
174+
protocol = "IPv6"
175+
}
176+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "would extend prefix to %d bits, which is too long for an %s address", length, protocol)
177+
}
178+
179+
next, rollover := cidr.NextSubnet(current, length)
180+
if rollover || !network.Contains(next.IP) {
181+
// If we run out of suffix bits in the base CIDR prefix then
182+
// NextSubnet will start incrementing the prefix bits, which
183+
// we don't allow because it would then allocate addresses
184+
// outside of the caller's given prefix.
185+
return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "not enough remaining address space for a subnet with a prefix of %d bits after %s", length, current.String())
186+
}
187+
188+
current = next
189+
retVals[i] = cty.StringVal(current.String())
190+
}
191+
192+
return cty.ListVal(retVals), nil
193+
},
194+
})
195+
116196
// CidrHost calculates a full host IP address within a given IP network address prefix.
117197
func CidrHost(prefix, hostnum cty.Value) (cty.Value, error) {
118198
return CidrHostFunc.Call([]cty.Value{prefix, hostnum})
@@ -127,3 +207,12 @@ func CidrNetmask(prefix cty.Value) (cty.Value, error) {
127207
func CidrSubnet(prefix, newbits, netnum cty.Value) (cty.Value, error) {
128208
return CidrSubnetFunc.Call([]cty.Value{prefix, newbits, netnum})
129209
}
210+
211+
// CidrSubnets calculates a sequence of consecutive subnet prefixes that may
212+
// be of different prefix lengths under a common base prefix.
213+
func CidrSubnets(prefix cty.Value, newbits ...cty.Value) (cty.Value, error) {
214+
args := make([]cty.Value, len(newbits)+1)
215+
args[0] = prefix
216+
copy(args[1:], newbits)
217+
return CidrSubnetsFunc.Call(args)
218+
}

internal/lang/funcs/cidr_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,107 @@ func TestCidrSubnet(t *testing.T) {
214214
})
215215
}
216216
}
217+
func TestCidrSubnets(t *testing.T) {
218+
tests := []struct {
219+
Prefix cty.Value
220+
Newbits []cty.Value
221+
Want cty.Value
222+
Err string
223+
}{
224+
{
225+
cty.StringVal("10.0.0.0/21"),
226+
[]cty.Value{
227+
cty.NumberIntVal(3),
228+
cty.NumberIntVal(3),
229+
cty.NumberIntVal(3),
230+
cty.NumberIntVal(4),
231+
cty.NumberIntVal(4),
232+
cty.NumberIntVal(4),
233+
cty.NumberIntVal(7),
234+
cty.NumberIntVal(7),
235+
cty.NumberIntVal(7),
236+
},
237+
cty.ListVal([]cty.Value{
238+
cty.StringVal("10.0.0.0/24"),
239+
cty.StringVal("10.0.1.0/24"),
240+
cty.StringVal("10.0.2.0/24"),
241+
cty.StringVal("10.0.3.0/25"),
242+
cty.StringVal("10.0.3.128/25"),
243+
cty.StringVal("10.0.4.0/25"),
244+
cty.StringVal("10.0.4.128/28"),
245+
cty.StringVal("10.0.4.144/28"),
246+
cty.StringVal("10.0.4.160/28"),
247+
}),
248+
``,
249+
},
250+
{
251+
cty.StringVal("10.0.0.0/30"),
252+
[]cty.Value{
253+
cty.NumberIntVal(1),
254+
cty.NumberIntVal(3),
255+
},
256+
cty.UnknownVal(cty.List(cty.String)),
257+
`would extend prefix to 33 bits, which is too long for an IPv4 address`,
258+
},
259+
{
260+
cty.StringVal("10.0.0.0/8"),
261+
[]cty.Value{
262+
cty.NumberIntVal(1),
263+
cty.NumberIntVal(1),
264+
cty.NumberIntVal(1),
265+
},
266+
cty.UnknownVal(cty.List(cty.String)),
267+
`not enough remaining address space for a subnet with a prefix of 9 bits after 10.128.0.0/9`,
268+
},
269+
{
270+
cty.StringVal("10.0.0.0/8"),
271+
[]cty.Value{
272+
cty.NumberIntVal(1),
273+
cty.NumberIntVal(0),
274+
},
275+
cty.UnknownVal(cty.List(cty.String)),
276+
`must extend prefix by at least one bit`,
277+
},
278+
{
279+
cty.StringVal("10.0.0.0/8"),
280+
[]cty.Value{
281+
cty.NumberIntVal(1),
282+
cty.NumberIntVal(-1),
283+
},
284+
cty.UnknownVal(cty.List(cty.String)),
285+
`must extend prefix by at least one bit`,
286+
},
287+
{
288+
cty.StringVal("fe80::/48"),
289+
[]cty.Value{
290+
cty.NumberIntVal(1),
291+
cty.NumberIntVal(33),
292+
},
293+
cty.UnknownVal(cty.List(cty.String)),
294+
`may not extend prefix by more than 32 bits`,
295+
},
296+
}
297+
298+
for _, test := range tests {
299+
t.Run(fmt.Sprintf("cidrsubnets(%#v, %#v)", test.Prefix, test.Newbits), func(t *testing.T) {
300+
got, err := CidrSubnets(test.Prefix, test.Newbits...)
301+
wantErr := test.Err != ""
302+
303+
if wantErr {
304+
if err == nil {
305+
t.Fatal("succeeded; want error")
306+
}
307+
if err.Error() != test.Err {
308+
t.Fatalf("wrong error\ngot: %s\nwant: %s", err.Error(), test.Err)
309+
}
310+
return
311+
} else if err != nil {
312+
t.Fatalf("unexpected error: %s", err)
313+
}
314+
315+
if !got.RawEquals(test.Want) {
316+
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
317+
}
318+
})
319+
}
320+
}

internal/lang/functions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (s *Scope) Functions() map[string]function.Function {
4242
"cidrhost": funcs.CidrHostFunc,
4343
"cidrnetmask": funcs.CidrNetmaskFunc,
4444
"cidrsubnet": funcs.CidrSubnetFunc,
45+
"cidrsubnets": funcs.CidrSubnetsFunc,
4546
"coalesce": funcs.CoalesceFunc,
4647
"coalescelist": funcs.CoalesceListFunc,
4748
"compact": funcs.CompactFunc,

internal/lang/functions_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,18 @@ func TestFunctions(t *testing.T) {
161161
},
162162
},
163163

164+
"cidrsubnets": {
165+
{
166+
`cidrsubnets("10.0.0.0/8", 8, 8, 16, 8)`,
167+
cty.ListVal([]cty.Value{
168+
cty.StringVal("10.0.0.0/16"),
169+
cty.StringVal("10.1.0.0/16"),
170+
cty.StringVal("10.2.0.0/24"),
171+
cty.StringVal("10.3.0.0/16"),
172+
}),
173+
},
174+
},
175+
164176
"coalesce": {
165177
{
166178
`coalesce("first", "second", "third")`,

0 commit comments

Comments
 (0)