From b6d3d69d3a639b50b50bfbe1e9a6bdf1c09aaeb2 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 23 May 2018 15:42:04 -0700 Subject: [PATCH] port cidr functions --- lang/funcs/cidr.go | 129 +++++++++++ lang/funcs/cidr_test.go | 216 ++++++++++++++++++ lang/functions.go | 14 +- .../functions/cidrsubnet.html.md | 2 +- 4 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 lang/funcs/cidr.go create mode 100644 lang/funcs/cidr_test.go diff --git a/lang/funcs/cidr.go b/lang/funcs/cidr.go new file mode 100644 index 000000000..2680790bb --- /dev/null +++ b/lang/funcs/cidr.go @@ -0,0 +1,129 @@ +package funcs + +import ( + "fmt" + "net" + + "github.com/apparentlymart/go-cidr/cidr" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/gocty" +) + +// CidrHostFunc contructs a function that calculates a full host IP address +// within a given IP network address prefix. +var CidrHostFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + { + Name: "hostnum", + Type: cty.Number, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var hostNum int + if err := gocty.FromCtyValue(args[1], &hostNum); err != nil { + return cty.UnknownVal(cty.String), err + } + _, network, err := net.ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) + } + + ip, err := cidr.Host(network, hostNum) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(ip.String()), nil + }, +}) + +// CidrNetmaskFunc contructs a function that converts an IPv4 address prefix given +// in CIDR notation into a subnet mask address. +var CidrNetmaskFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + _, network, err := net.ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) + } + + return cty.StringVal(net.IP(network.Mask).String()), nil + }, +}) + +// CidrSubnetFunc contructs a function that calculates a subnet address within +// a given IP network address prefix. +var CidrSubnetFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + { + Name: "newbits", + Type: cty.Number, + }, + { + Name: "netnum", + Type: cty.Number, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var newbits int + if err := gocty.FromCtyValue(args[1], &newbits); err != nil { + return cty.UnknownVal(cty.String), err + } + var netnum int + if err := gocty.FromCtyValue(args[2], &netnum); err != nil { + return cty.UnknownVal(cty.String), err + } + + _, network, err := net.ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), 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 newbits > 32 { + return cty.UnknownVal(cty.String), fmt.Errorf("may not extend prefix by more than 32 bits") + } + + newNetwork, err := cidr.Subnet(network, newbits, netnum) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(newNetwork.String()), nil + }, +}) + +// CidrHost calculates a full host IP address within a given IP network address prefix. +func CidrHost(prefix, hostnum cty.Value) (cty.Value, error) { + return CidrHostFunc.Call([]cty.Value{prefix, hostnum}) +} + +// CidrNetmask converts an IPv4 address prefix given in CIDR notation into a subnet mask address. +func CidrNetmask(prefix cty.Value) (cty.Value, error) { + return CidrNetmaskFunc.Call([]cty.Value{prefix}) +} + +// CidrSubnet calculates a subnet address within a given IP network address prefix. +func CidrSubnet(prefix, newbits, netnum cty.Value) (cty.Value, error) { + return CidrSubnetFunc.Call([]cty.Value{prefix, newbits, netnum}) +} diff --git a/lang/funcs/cidr_test.go b/lang/funcs/cidr_test.go new file mode 100644 index 000000000..e9fd6c7a3 --- /dev/null +++ b/lang/funcs/cidr_test.go @@ -0,0 +1,216 @@ +package funcs + +import ( + "fmt" + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestCidrHost(t *testing.T) { + tests := []struct { + Prefix cty.Value + Hostnum cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("192.168.1.0/24"), + cty.NumberIntVal(5), + cty.StringVal("192.168.1.5"), + false, + }, + { + cty.StringVal("192.168.1.0/24"), + cty.NumberIntVal(-5), + cty.StringVal("192.168.1.251"), + false, + }, + { + cty.StringVal("192.168.1.0/24"), + cty.NumberIntVal(-256), + cty.StringVal("192.168.1.0"), + false, + }, + { + cty.StringVal("192.168.1.0/30"), + cty.NumberIntVal(255), + cty.UnknownVal(cty.String), + true, // 255 doesn't fit in two bits + }, + { + cty.StringVal("192.168.1.0/30"), + cty.NumberIntVal(-255), + cty.UnknownVal(cty.String), + true, // 255 doesn't fit in two bits + }, + { + cty.StringVal("not-a-cidr"), + cty.NumberIntVal(6), + cty.UnknownVal(cty.String), + true, // not a valid CIDR mask + }, + { + cty.StringVal("10.256.0.0/8"), + cty.NumberIntVal(6), + cty.UnknownVal(cty.String), + true, // can't have an octet >255 + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("cidrhost(%#v, %#v)", test.Prefix, test.Hostnum), func(t *testing.T) { + got, err := CidrHost(test.Prefix, test.Hostnum) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestCidrNetmask(t *testing.T) { + tests := []struct { + Prefix cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("192.168.1.0/24"), + cty.StringVal("255.255.255.0"), + false, + }, + { + cty.StringVal("192.168.1.0/32"), + cty.StringVal("255.255.255.255"), + false, + }, + { + cty.StringVal("0.0.0.0/0"), + cty.StringVal("0.0.0.0"), + false, + }, + { + cty.StringVal("1::/64"), + cty.StringVal("ffff:ffff:ffff:ffff::"), + false, + }, + { + cty.StringVal("not-a-cidr"), + cty.UnknownVal(cty.String), + true, // not a valid CIDR mask + }, + { + cty.StringVal("110.256.0.0/8"), + cty.UnknownVal(cty.String), + true, // can't have an octet >255 + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("cidrnetmask(%#v)", test.Prefix), func(t *testing.T) { + got, err := CidrNetmask(test.Prefix) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestCidrSubnet(t *testing.T) { + tests := []struct { + Prefix cty.Value + Newbits cty.Value + Netnum cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("192.168.2.0/20"), + cty.NumberIntVal(4), + cty.NumberIntVal(6), + cty.StringVal("192.168.6.0/24"), + false, + }, + { + cty.StringVal("fe80::/48"), + cty.NumberIntVal(16), + cty.NumberIntVal(6), + cty.StringVal("fe80:0:0:6::/64"), + false, + }, + { // IPv4 address encoded in IPv6 syntax gets normalized + cty.StringVal("::ffff:192.168.0.0/112"), + cty.NumberIntVal(8), + cty.NumberIntVal(6), + cty.StringVal("192.168.6.0/24"), + false, + }, + { // not enough bits left + cty.StringVal("192.168.0.0/30"), + cty.NumberIntVal(4), + cty.NumberIntVal(6), + cty.UnknownVal(cty.String), + true, + }, + { // can't encode 16 in 2 bits + cty.StringVal("192.168.0.0/168"), + cty.NumberIntVal(2), + cty.NumberIntVal(16), + cty.StringVal("fe80:0:0:6::/64"), + true, + }, + { // not a valid CIDR mask + cty.StringVal("not-a-cidr"), + cty.NumberIntVal(4), + cty.NumberIntVal(6), + cty.StringVal("fe80:0:0:6::/64"), + true, + }, + { // can't have an octet >255 + cty.StringVal("10.256.0.0/8"), + cty.NumberIntVal(4), + cty.NumberIntVal(6), + cty.StringVal("fe80:0:0:6::/64"), + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("cidrsubnet(%#v, %#v, %#v)", test.Prefix, test.Newbits, test.Netnum), func(t *testing.T) { + got, err := CidrSubnet(test.Prefix, test.Newbits, test.Netnum) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index 9e3a4b8c0..0863ef6b3 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -39,9 +39,9 @@ func (s *Scope) Functions() map[string]function.Function { "bcrypt": funcs.BcryptFunc, "ceil": funcs.CeilFunc, "chomp": funcs.ChompFunc, - "cidrhost": unimplFunc, // TODO - "cidrnetmask": unimplFunc, // TODO - "cidrsubnet": unimplFunc, // TODO + "cidrhost": funcs.CidrHostFunc, + "cidrnetmask": funcs.CidrNetmaskFunc, + "cidrsubnet": funcs.CidrSubnetFunc, "coalesce": stdlib.CoalesceFunc, "coalescelist": unimplFunc, // TODO "compact": unimplFunc, // TODO @@ -59,8 +59,8 @@ func (s *Scope) Functions() map[string]function.Function { "floor": funcs.FloorFunc, "format": stdlib.FormatFunc, "formatlist": stdlib.FormatListFunc, - "indent": unimplFunc, // TODO - "index": funcs.IndentFunc, + "indent": funcs.IndentFunc, + "index": unimplFunc, // TODO "join": funcs.JoinFunc, "jsondecode": stdlib.JSONDecodeFunc, "jsonencode": stdlib.JSONEncodeFunc, @@ -82,7 +82,7 @@ func (s *Scope) Functions() map[string]function.Function { "sha1": funcs.Sha1Func, "sha256": funcs.Sha256Func, "sha512": funcs.Sha512Func, - "signum": unimplFunc, // TODO + "signum": funcs.SignumFunc, "slice": unimplFunc, // TODO "sort": funcs.SortFunc, "split": funcs.SplitFunc, @@ -91,7 +91,7 @@ func (s *Scope) Functions() map[string]function.Function { "timeadd": funcs.TimeAddFunc, "title": funcs.TitleFunc, "transpose": unimplFunc, // TODO - "trimspace": funcs.TrimSpace, + "trimspace": funcs.TrimSpaceFunc, "upper": stdlib.UpperFunc, "urlencode": funcs.URLEncodeFunc, "uuid": funcs.UUIDFunc, diff --git a/website/docs/configuration/functions/cidrsubnet.html.md b/website/docs/configuration/functions/cidrsubnet.html.md index 34a9c994d..efc2ed418 100644 --- a/website/docs/configuration/functions/cidrsubnet.html.md +++ b/website/docs/configuration/functions/cidrsubnet.html.md @@ -9,7 +9,7 @@ description: |- # `cidrsubnet` Function -`cidrhost` calculates a subnet address within given IP network address prefix. +`cidrsubnet` calculates a subnet address within given IP network address prefix. ```hcl cidrsubnet(prefix, newbits, netnum)