Skip to content

Commit cfacf6f

Browse files
authored
Add cidr* functions based on Terraform IP network functions (#34)
1 parent 2965789 commit cfacf6f

File tree

9 files changed

+293
-0
lines changed

9 files changed

+293
-0
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ All syntax and functions:
148148
- `readFile` - reads a file from a path, relative paths are translated to absolute paths, based on `root` function or property
149149
- `writeFile` - writes a file to a path, relative paths are translated to absolute paths, based on `root` function or property
150150
- `root` - the root path, used for relative to absolute path translation in any file based operations; by default `PWD` is used
151+
- `cidrHost` - calculates a full host IP address for a given host number within a given IP network address prefix
152+
- `cidrNetmask` - converts an IPv4 address prefix given in CIDR notation into a subnet mask address
153+
- `cidrSubnets` - calculates a subnet address within given IP network address prefix
154+
- `cidrSubnetSizes` - calculates a sequence of consecutive IP address ranges within a particular CIDR prefix
151155

152156
See also [examples](examples) and a more
153157
[detailed documentation](https://godoc.org/github.com/VirtusLab/render/renderer#Renderer.ExtraFunctions).

examples/cidr.yaml.expected

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
10.12.112.16
2+
10.12.113.12
3+
fd00:fd12:3456:7890::22
4+
5+
255.240.0.0
6+
7+
10.0.0.0/18
8+
10.0.64.0/18
9+
10.0.160.0/19
10+
11+
12+
0 10.1.0.0/20
13+
1 10.1.16.0/20
14+
2 10.1.32.0/24
15+
3 10.1.48.0/20
16+
17+
0 fd00:fd12:3456:7800::/72
18+
1 fd00:fd12:3456:7800:100::/72
19+
2 fd00:fd12:3456:7800:200::/72
20+
3 fd00:fd12:3456:7800:300::/88

examples/cidr.yaml.tmpl

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{ cidrHost 16 "10.12.127.0/20" }}
2+
{{ cidrHost 268 "10.12.127.0/20" }}
3+
{{ "fd00:fd12:3456:7890:00a2::/72" | cidrHost 34 }}
4+
5+
{{ cidrNetmask "10.0.0.0/12" }}
6+
7+
{{ index (cidrSubnets 2 "10.0.0.0/16") 0 }}
8+
{{ index ("10.0.0.0/16" | cidrSubnets 2) 1 }}
9+
{{ index (cidrSubnets 3 "10.0.0.0/16") 5 }}
10+
11+
{{ range $k, $v := cidrSubnetSizes 4 4 8 4 "10.1.0.0/16" }}
12+
{{ $k }} {{ $v }}{{ end }}
13+
{{ range $k, $v := cidrSubnetSizes 16 16 16 32 "fd00:fd12:3456:7890::/56" }}
14+
{{ $k }} {{ $v }}{{ end }}

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/Masterminds/sprig/v3 v3.2.2
77
github.com/VirtusLab/crypt v0.2.6
88
github.com/VirtusLab/go-extended v0.0.11
9+
github.com/apparentlymart/go-cidr v1.1.0
910
github.com/ghodss/yaml v1.0.0
1011
github.com/imdario/mergo v0.3.12
1112
github.com/pkg/errors v0.9.1

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ github.com/VirtusLab/crypt v0.2.6/go.mod h1:gR1pcGS9+4c5xTz+eXtN9Rls6ZmTqV0LOTCp
8888
github.com/VirtusLab/go-extended v0.0.11 h1:1m+C4YV4ujx/YeUjSTyxziKYc0QZnS68ZD3qu4y8Opo=
8989
github.com/VirtusLab/go-extended v0.0.11/go.mod h1:7eU6FdGOELNqTcxnyl05JH4HQGS6326PCURP4uzpR3c=
9090
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
91+
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
92+
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
9193
github.com/aws/aws-sdk-go v1.43.17 h1:jDPBz1UuTxmyRo0eLgaRiro0fiI1zL7lkscqYxoEDLM=
9294
github.com/aws/aws-sdk-go v1.43.17/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
9395
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ func action(c *cli.Context) error {
162162
renderer.WithSprigFunctions(),
163163
renderer.WithExtraFunctions(),
164164
renderer.WithCryptFunctions(),
165+
renderer.WithNetFunctions(),
165166
)
166167

167168
// check for extra args after vars and configs were parsed to avoid confusing error messages

renderer/examples_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,93 @@ func ExampleN_empty() {
240240
// Output:
241241
// 0
242242
}
243+
244+
func ExampleCidrHost_simple() {
245+
tmpl := `
246+
{{ cidrHost 16 "10.12.127.0/20" }}
247+
{{ cidrHost 268 "10.12.127.0/20" }}
248+
{{ cidrHost 34 "fd00:fd12:3456:7890:00a2::/72" }}
249+
{{ "10.12.127.0/20" | cidrHost 16 }}
250+
`
251+
result, err := renderer.New(
252+
renderer.WithNetFunctions(),
253+
).Render(tmpl)
254+
if err != nil {
255+
fmt.Println(err)
256+
}
257+
fmt.Println(result)
258+
// Output:
259+
// 10.12.112.16
260+
// 10.12.113.12
261+
// fd00:fd12:3456:7890::22
262+
// 10.12.112.16
263+
}
264+
265+
func ExampleCidrNetmask_simple() {
266+
tmpl := `
267+
{{ cidrNetmask "10.0.0.0/12" }}
268+
`
269+
result, err := renderer.New(
270+
renderer.WithNetFunctions(),
271+
).Render(tmpl)
272+
if err != nil {
273+
fmt.Println(err)
274+
}
275+
fmt.Println(result)
276+
// Output:
277+
// 255.240.0.0
278+
}
279+
280+
func ExampleCidrSubnets_simple() {
281+
tmpl := `
282+
{{ index (cidrSubnets 2 "10.0.0.0/16") 0 }}
283+
{{ index ("10.0.0.0/16" | cidrSubnets 2) 1 }}
284+
{{ range cidrSubnets 3 "10.0.0.0/16" }}
285+
{{ . }}{{ end }}
286+
`
287+
result, err := renderer.New(
288+
renderer.WithNetFunctions(),
289+
).Render(tmpl)
290+
if err != nil {
291+
fmt.Println(err)
292+
}
293+
fmt.Println(result)
294+
// Output:
295+
// 10.0.0.0/18
296+
// 10.0.64.0/18
297+
//
298+
// 10.0.0.0/19
299+
// 10.0.32.0/19
300+
// 10.0.64.0/19
301+
// 10.0.96.0/19
302+
// 10.0.128.0/19
303+
// 10.0.160.0/19
304+
// 10.0.192.0/19
305+
// 10.0.224.0/19
306+
}
307+
308+
func ExampleCidrSubnetSizes_simple() {
309+
tmpl := `
310+
{{ range $k, $v := cidrSubnetSizes 4 4 8 4 "10.1.0.0/16" }}
311+
{{ $k }} {{ $v }}{{ end }}
312+
{{ range $k, $v := cidrSubnetSizes 16 16 16 32 "fd00:fd12:3456:7890::/56" }}
313+
{{ $k }} {{ $v }}{{ end }}
314+
`
315+
result, err := renderer.New(
316+
renderer.WithNetFunctions(),
317+
).Render(tmpl)
318+
if err != nil {
319+
fmt.Println(err)
320+
}
321+
fmt.Println(result)
322+
// Output:
323+
// 0 10.1.0.0/20
324+
// 1 10.1.16.0/20
325+
// 2 10.1.32.0/24
326+
// 3 10.1.48.0/20
327+
//
328+
// 0 fd00:fd12:3456:7800::/72
329+
// 1 fd00:fd12:3456:7800:100::/72
330+
// 2 fd00:fd12:3456:7800:200::/72
331+
// 3 fd00:fd12:3456:7800:300::/88
332+
}

renderer/functions.go

+145
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import (
55
"compress/gzip"
66
"io"
77
"io/ioutil"
8+
"math/big"
9+
"net"
810
"os"
911
"path/filepath"
1012
"reflect"
1113
"strings"
1214

1315
"github.com/VirtusLab/render/renderer/parameters"
16+
"github.com/apparentlymart/go-cidr/cidr"
1417

1518
"github.com/VirtusLab/go-extended/pkg/files"
1619
json2 "github.com/VirtusLab/go-extended/pkg/json"
@@ -215,3 +218,145 @@ func asBytes(input interface{}) ([]byte, error) {
215218
return nil, errors.Errorf("expected []byte or string, got: '%v'", reflect.TypeOf(input))
216219
}
217220
}
221+
222+
func parseCIDR(prefix interface{}) (*net.IPNet, error) {
223+
if n, ok := prefix.(*net.IPNet); ok {
224+
return n, nil
225+
}
226+
if n, ok := prefix.(string); ok {
227+
_, network, err := net.ParseCIDR(n)
228+
return network, err
229+
}
230+
231+
return nil, errors.Errorf("cannot parse CIDR: %v", prefix)
232+
}
233+
234+
func toInts(in ...interface{}) []int {
235+
out := make([]int, len(in))
236+
for i, v := range in {
237+
if vv, ok := v.(int); ok {
238+
out[i] = vv
239+
}
240+
}
241+
return out
242+
}
243+
244+
// CidrHost calculates a full host IP address within a given IP network address prefix.
245+
func CidrHost(hostnum int, prefix interface{}) (*net.IP, error) {
246+
logrus.Debug("hostnum: ", hostnum)
247+
logrus.Debug("prefix: ", prefix)
248+
249+
network, err := parseCIDR(prefix)
250+
if err != nil {
251+
return nil, err
252+
}
253+
254+
ip, err := cidr.HostBig(network, big.NewInt(int64(hostnum)))
255+
return &ip, err
256+
}
257+
258+
// CidrNetmask converts an IPv4 address prefix given in CIDR notation into a subnet mask address.
259+
func CidrNetmask(prefix interface{}) (*net.IP, error) {
260+
logrus.Debug("prefix: ", prefix)
261+
262+
network, err := parseCIDR(prefix)
263+
if err != nil {
264+
return nil, err
265+
}
266+
267+
if len(network.IP) != net.IPv4len {
268+
return nil, errors.Errorf("only IPv4 networks are supported")
269+
}
270+
271+
netmask := net.IP(network.Mask)
272+
return &netmask, nil
273+
}
274+
275+
// CidrSubnets calculates a subnet address within a given IP network address prefix.
276+
func CidrSubnets(newbits int, prefix interface{}) ([]*net.IPNet, error) {
277+
logrus.Debug("newbits: ", newbits)
278+
logrus.Debug("prefix: ", prefix)
279+
280+
network, err := parseCIDR(prefix)
281+
if err != nil {
282+
return nil, err
283+
}
284+
285+
if newbits < 1 {
286+
return nil, errors.Errorf("must extend prefix by at least one bit")
287+
}
288+
289+
maxnetnum := int64(1 << uint64(newbits))
290+
retValues := make([]*net.IPNet, maxnetnum)
291+
for i := int64(0); i < maxnetnum; i++ {
292+
subnet, err := cidr.SubnetBig(network, newbits, big.NewInt(i))
293+
if err != nil {
294+
return nil, err
295+
}
296+
retValues[i] = subnet
297+
}
298+
299+
return retValues, nil
300+
}
301+
302+
// CidrSubnetSizes calculates a sequence of consecutive subnet prefixes that may
303+
// be of different prefix lengths under a common base prefix.
304+
func CidrSubnetSizes(args ...interface{}) ([]*net.IPNet, error) {
305+
logrus.Debug("args: ", args)
306+
307+
if len(args) < 2 {
308+
return nil, errors.Errorf("wrong number of args: want 2 or more, got %d", len(args))
309+
}
310+
311+
network, err := parseCIDR(args[len(args)-1])
312+
if err != nil {
313+
return nil, err
314+
}
315+
newbits := toInts(args[:len(args)-1]...)
316+
317+
startPrefixLen, _ := network.Mask.Size()
318+
firstLength := newbits[0]
319+
320+
firstLength += startPrefixLen
321+
retValues := make([]*net.IPNet, len(newbits))
322+
323+
current, _ := cidr.PreviousSubnet(network, firstLength)
324+
325+
for i, length := range newbits {
326+
if length < 1 {
327+
return nil, errors.Errorf("must extend prefix by at least one bit")
328+
}
329+
// For portability with 32-bit systems where the subnet number
330+
// will be a 32-bit int, we only allow extension of 32 bits in
331+
// one call even if we're running on a 64-bit machine.
332+
// (Of course, this is significant only for IPv6.)
333+
if length > 32 {
334+
return nil, errors.Errorf("may not extend prefix by more than 32 bits")
335+
}
336+
337+
length += startPrefixLen
338+
if length > (len(network.IP) * 8) {
339+
protocol := "IP"
340+
switch len(network.IP) {
341+
case net.IPv4len:
342+
protocol = "IPv4"
343+
case net.IPv6len:
344+
protocol = "IPv6"
345+
}
346+
return nil, errors.Errorf("would extend prefix to %d bits, which is too long for an %s address", length, protocol)
347+
}
348+
349+
next, rollover := cidr.NextSubnet(current, length)
350+
if rollover || !network.Contains(next.IP) {
351+
// If we run out of suffix bits in the base CIDR prefix then
352+
// NextSubnet will start incrementing the prefix bits, which
353+
// we don't allow because it would then allocate addresses
354+
// outside of the caller's given prefix.
355+
return nil, errors.Errorf("not enough remaining address space for a subnet with a prefix of %d bits after %s", length, current.String())
356+
}
357+
current = next
358+
retValues[i] = current
359+
}
360+
361+
return retValues, nil
362+
}

renderer/render.go

+16
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ func WithCryptFunctions() func(*config.Config) {
109109
return WithMoreFunctions(crypto.TemplateFunctions())
110110
}
111111

112+
// WithNetFunctions mutates Renderer configuration by merging the custom template functions
113+
func WithNetFunctions() func(*config.Config) {
114+
return WithMoreFunctions(NetFunctions())
115+
}
116+
112117
// MergeFunctions merges two template.FuncMap instances, overrides if necessary
113118
func MergeFunctions(dst *template.FuncMap, src template.FuncMap) error {
114119
err := mergo.Merge(dst, src, mergo.WithOverride)
@@ -233,6 +238,17 @@ func ExtraFunctions() template.FuncMap {
233238
}
234239
}
235240

241+
// NetFunctions provides additional template functions
242+
// to the standard (text/template) ones
243+
func NetFunctions() template.FuncMap {
244+
return template.FuncMap{
245+
"cidrHost": CidrHost,
246+
"cidrNetmask": CidrNetmask,
247+
"cidrSubnets": CidrSubnets,
248+
"cidrSubnetSizes": CidrSubnetSizes,
249+
}
250+
}
251+
236252
// TODO move to files package
237253
type dirEntry struct {
238254
path string

0 commit comments

Comments
 (0)