From 3537f5f5c479cac9898fd05122403b5acaf39fdd Mon Sep 17 00:00:00 2001 From: Oleh Neichev <59373462+BonySmoke@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:20:48 +0200 Subject: [PATCH] feat: Add IP Network to supported HCL Functions (#2426) * Add IP Network to supported HCL Functions This patch adds CIDR functions from the `go-cty-funcs` package to supported HCL functions Signed-off-by: Oleh Neichev * Test HCL CIDR Functions Signed-off-by: Oleh Neichev --------- Signed-off-by: Oleh Neichev --- docs/hcl_funcs.md | 160 ++++++++++++++++++++------------- docs/index.md | 6 +- go.mod | 2 + go.sum | 4 + pkg/hcllang/hcl_functions.go | 5 ++ pkg/hcllang/hcl_loader_test.go | 23 +++++ pkg/hcllang/testdata/cidr.hcl | 7 ++ 7 files changed, 143 insertions(+), 64 deletions(-) create mode 100644 pkg/hcllang/testdata/cidr.hcl diff --git a/docs/hcl_funcs.md b/docs/hcl_funcs.md index 0f54183f..e5be51d5 100644 --- a/docs/hcl_funcs.md +++ b/docs/hcl_funcs.md @@ -24,22 +24,60 @@ map = { } can1 = can(hv.map.myVar) # true -can2 = can(hv.map.notMyVar) +can2 = can(hv.map.notMyVar) # false ``` +#### cidrhost +`cidrhost` calculates a full host IP address for a given host number within a given IP network address prefix. +``` +cidrhost(prefix, hostnum) +``` +``` +cidrhost("10.0.0.0/8", 2) +# 10.0.0.2 +cidrhost("10.0.0.0/8", -2) +# 10.255.255.254 +``` +#### cidrnetmask +`cidrnetmask` converts an IPv4 address prefix given in CIDR notation into a subnet mask address. +``` +cidrnetmask(prefix) +``` +``` +cidrnetmask("10.0.0.0/16") +# 255.255.0.0 +``` +#### cidrsubnet +`cidrsubnet` calculates a subnet address within a given IP network address prefix. +``` +cidrsubnet(prefix, newbits, netnum) +``` +``` +cidrsubnet("10.0.0.0/8", 8, 2) +# 10.2.0.0/16 +``` +#### cidrsubnets +`cidrsubnets` calculates a sequence of consecutive IP address ranges within a particular CIDR prefix. +``` +cidrsubnets(prefix, newbits...) +``` +``` +cidrsubnets("10.0.0.0/8", 4, 4) +# ["10.0.0.0/12", "10.16.0.0/12"] +``` #### ceil `ceil` returns the ceiling value of a given number ``` ceil(number) ``` ``` -ceil(1) +ceil(1) # 1 -ceil(1.1) +ceil(1.1) # 2 ``` #### chomp -`chomp` removes newline characters at the end of a string. +`chomp` removes newline characters at the end of a string. ``` chomp(string) ``` @@ -66,7 +104,7 @@ coalesce([null, "value"]...) # value ``` #### coalescelist -`coalescelist` takes any number of list arguments and returns the first one that isn't empty. +`coalescelist` takes any number of list arguments and returns the first one that isn't empty. ``` coalescelist(list) ``` @@ -89,7 +127,7 @@ compact(["", "val1", "val2"]) # ["val1", "val2"] ``` #### concat -`concat` takes one or more sequences (lists or tuples) and returns the single sequence that results from concatenating them together in order. +`concat` takes one or more sequences (lists or tuples) and returns the single sequence that results from concatenating them together in order. ``` concat(list, list...) ``` @@ -145,7 +183,7 @@ element(["val1","val2"], 1) ``` #### chunklist -`chunklist` splits a single list into fixed-size chunks, returning a list of lists. +`chunklist` splits a single list into fixed-size chunks, returning a list of lists. ``` chunklist(list, size) ``` @@ -154,7 +192,7 @@ chunklist(["a","b"], 1) # [["a"], ["b"]] ``` #### flatten -`flatten` takes a list and replaces any elements that are lists with a flattened sequence of the list contents. +`flatten` takes a list and replaces any elements that are lists with a flattened sequence of the list contents. ``` flatten(list) ``` @@ -163,7 +201,7 @@ flatten([["a"], ["a","b"], ["c"]]) # ["a","a","b","c"] ``` #### floor -`floor` returns the closest whole number lesser than or equal to the given value. +`floor` returns the closest whole number lesser than or equal to the given value. ``` floor(number) ``` @@ -182,7 +220,7 @@ format(format, values) ``` format("Hello %s", "world") # Hello world -``` +``` #### formatdate `formatdate` reformats a timestamp given in RFC3339 syntax into another time syntax defined by a given format string. [Syntax details](https://pkg.go.dev/github.com/zclconf/go-cty/cty/function/stdlib#FormatDate) @@ -215,7 +253,7 @@ formatlist("%s %s", "hello", ["World", "You"]) ] ``` #### indent -`indent` adds a given number of spaces to the beginnings of all but the first line in a given multi-line string. +`indent` adds a given number of spaces to the beginnings of all but the first line in a given multi-line string. ``` indent(number, string) ``` @@ -239,7 +277,7 @@ int(6.2) ``` #### join -`join` concatenates together the string elements of one or more lists with a given separator. +`join` concatenates together the string elements of one or more lists with a given separator. ``` join(listOfStrings, separator) ``` @@ -249,7 +287,7 @@ join(" ", ["hello", "world"]) ``` #### jsondecode -`jsondecode` parses the given JSON string and, if it is valid, returns the value it represents. +`jsondecode` parses the given JSON string and, if it is valid, returns the value it represents. ``` jsonencode(string) ``` @@ -260,7 +298,7 @@ jsonencode({"hello"="world"}) ``` #### jsonencode -`jsonencode` returns a JSON serialization of the given value. +`jsonencode` returns a JSON serialization of the given value. ``` jsondecode(string) ``` @@ -275,13 +313,13 @@ jsondecode("{\"hello\": \"world\"}") ``` keys(map) ``` - + ``` keys({val1=1, val2=2, val3=3}) # ["val1","val2","val3"] ``` #### length -`length` returns the number of elements in the given __collection__. +`length` returns the number of elements in the given __collection__. See `strlen` for strings ``` length(list) @@ -292,7 +330,7 @@ length([1,2,3]) # 3 ``` #### log -`log` returns returns the logarithm of a given number in a given base. +`log` returns returns the logarithm of a given number in a given base. ``` log(number, base) ``` @@ -302,7 +340,7 @@ log(1, 10) # 0 ``` #### lookup -`lookup` performs a dynamic lookup into a map. There are three required arguments, inputMap and key, plus a defaultValue, which is a value to return if the given key is not found in the inputMap. +`lookup` performs a dynamic lookup into a map. There are three required arguments, inputMap and key, plus a defaultValue, which is a value to return if the given key is not found in the inputMap. ``` lookup(inputMap, key, defaultValue) ``` @@ -325,7 +363,7 @@ lower("HELLO world") # hello world ``` #### max -`max` returns the maximum number from the given numbers. +`max` returns the maximum number from the given numbers. ``` max(numbers) ``` @@ -346,7 +384,7 @@ merge({a="1"}, {a=[1,2], c="world"}, {d=40}) ``` #### min -`min` returns the minimum number from the given numbers. +`min` returns the minimum number from the given numbers. ``` min(numbers) ``` @@ -355,7 +393,7 @@ min(1,128,70) # 1 ``` #### parseint -`parseint` parses a string argument and returns an integer of the specified base. +`parseint` parses a string argument and returns an integer of the specified base. ``` parseint(string, base) ``` @@ -366,7 +404,7 @@ parseint("11001", 2) # 25 ``` #### pow -`pow` returns the logarithm of a given number in a given base. +`pow` returns the logarithm of a given number in a given base. ``` pow(number, power) ``` @@ -388,7 +426,7 @@ range(1, 10, 3) # [1, 4, 7] ``` #### regex -`regex` is a function that extracts one or more substrings from a given string by applying a regular expression pattern, describing the first match. +`regex` is a function that extracts one or more substrings from a given string by applying a regular expression pattern, describing the first match. The return type depends on the composition of the capture groups (if any) in the pattern: If there are no capture groups at all, the result is a single string representing the entire matched pattern. @@ -396,7 +434,7 @@ The return type depends on the composition of the capture groups (if any) in the If none of the capture groups are named, the result is a tuple whose elements are the sub-groups in order and whose values are their sub-matches, or null if a particular sub-group was inside another group that didn't match. It is invalid to use both named and un-named capture groups together in the same pattern. -If the pattern doesn't match, this function returns an error. To test for a match, call `regexall` and check if the length of the result is greater than zero. +If the pattern doesn't match, this function returns an error. To test for a match, call `regexall` and check if the length of the result is greater than zero. ``` regex(pattern, string) ``` @@ -410,7 +448,7 @@ regex("[0-9]+", "v1.2.3") The result type is always a list, whose element type is deduced from the pattern in the same way as the return type for Regex is decided. -If the pattern doesn't match at all, this function returns an empty list. +If the pattern doesn't match at all, this function returns an empty list. ``` regexall(pattern, string) ``` @@ -420,7 +458,7 @@ regexall("[0-9]+", "v1.2.3") # [1 2 3] ``` #### setintersection -`setintersection` returns a new set containing the elements that exist in all of the given sets, which must have element types that can all be converted to some common type using the standard type unification rules. If conversion is not possible, an error is returned. +`setintersection` returns a new set containing the elements that exist in all of the given sets, which must have element types that can all be converted to some common type using the standard type unification rules. If conversion is not possible, an error is returned. ``` setintersection(sets...) ``` @@ -436,7 +474,7 @@ setproduct(sets...) ``` ``` setproduct(["host1", "host2"], ["stg.domain", "prod.domain"]) -### +### [ [ "host1", @@ -455,37 +493,37 @@ setproduct(["host1", "host2"], ["stg.domain", "prod.domain"]) "prod.domain" ], ] -``` +``` #### setsubtract -`setsubtract` returns a new set containing the elements from the first set that are not present in the second set. The sets must have element types that can both be converted to some common type using the standard type unification rules. If conversion is not possible, an error is returned. +`setsubtract` returns a new set containing the elements from the first set that are not present in the second set. The sets must have element types that can both be converted to some common type using the standard type unification rules. If conversion is not possible, an error is returned. ``` setsubtract(sets...) ``` ``` setsubtract(["a", "b", "c"], ["a", "b"]) -### +### ["c"] -``` +``` #### setunion -`setunion` returns a new set containing all of the elements from the given sets, which must have element types that can all be converted to some common type using the standard type unification rules. If conversion is not possible, an error is returned. +`setunion` returns a new set containing all of the elements from the given sets, which must have element types that can all be converted to some common type using the standard type unification rules. If conversion is not possible, an error is returned. ``` setunion(sets...) ``` ``` setunion(["a", "b"], ["b", "c"], ["a", "d"]) -### +### ["a", "b", "c", "d"] -``` +``` #### signum -`signum` determines the sign of a number, returning a number between -1 and 1 to represent the sign. +`signum` determines the sign of a number, returning a number between -1 and 1 to represent the sign. ``` signum(number) ``` ``` signum(-182) # -1 -``` +``` #### slice `slice` extracts some consecutive elements from within a list. startIndex is inclusive, endIndex is exclusive @@ -497,7 +535,7 @@ slice([{"a" = "b"}, {"c" = "d"}, , {"e" = "f"}], 1, 1) # [] slice([{"a" = "b"}, {"c" = "d"}, {"e" = "f"}], 1, 2) # [{"c" = "d"}] -``` +``` #### sort `sort` re-orders the elements of a given list of strings so that they are in ascending lexicographical order. ``` @@ -506,25 +544,25 @@ sort(list) ``` sort(["1", "h", "r", "p", "word"]) # ["1", "h", "p", "r", "word"] -``` +``` #### split -`split` divides a given string by a given separator, returning a list of strings containing the characters between the separator sequences. +`split` divides a given string by a given separator, returning a list of strings containing the characters between the separator sequences. ``` split(separatorString, string) ``` ``` split(".", "host.domain") # ["host", "domain"] -``` +``` #### strlen -`strlen` is a Function that returns the length of the given string in characters. +`strlen` is a Function that returns the length of the given string in characters. ``` strlen(string) ``` ``` strlen("yes") # 3 -``` +``` #### strrev `strrev` is a Function that reverses the order of the characters in the given string. ``` @@ -533,16 +571,16 @@ strrev(string) ``` strrev("yes") # "sey" -``` +``` #### substr -`substr` is a Function that extracts a sequence of characters from another string and creates a new string. +`substr` is a Function that extracts a sequence of characters from another string and creates a new string. ``` substr(string, offsetNumber, length) ``` ``` substr("host.domain", 0, 4) # "host" -``` +``` #### timeadd `timeadd` adds a duration to a timestamp, returning a new timestamp. Only units "inferior" or equal to `h` are supported. @@ -553,7 +591,7 @@ substr(timestamp, duration) ``` timeadd("2024-01-01T00:00:00Z", "-2600h10m") # 2023-09-14T15:50:00Z -``` +``` #### trim `trim` removes the specified characters from the start and end of the given string. ``` @@ -562,16 +600,16 @@ trim(string, string) ``` trim("Can you do that ? Yes ?", "?") # "Can you do that ? Yes" -``` +``` #### trimprefix -`trimprefix` removes the specified prefix from the start of the given string. +`trimprefix` removes the specified prefix from the start of the given string. ``` trimprefix(stringToTrim, trimmingString) ``` ``` trimprefix("please, do it", "please, ") # "do it" -``` +``` #### trimspace `trimspace` removes any space characters from the start and end of the given string. ``` @@ -580,18 +618,18 @@ trimspace(string) ``` trimspace(" Hello World ") # "Hello World" -``` +``` #### trimsuffix -`trimsuffix` removes the specified suffix from the end of the given string. +`trimsuffix` removes the specified suffix from the end of the given string. ``` trimsuffix(stringToTrim, trimmingString) ``` ``` trimsuffix("Hello World", " World") # "Hello" -``` +``` #### try -`try` is a variadic function that tries to evaluate all of is arguments in sequence until one succeeds, in which case it returns that result, or returns an error if none of them succeed. +`try` is a variadic function that tries to evaluate all of is arguments in sequence until one succeeds, in which case it returns that result, or returns an error if none of them succeed. ``` try(expressions...) ``` @@ -604,16 +642,16 @@ values { try(hv.map.do_not_exist, hv.map.world) } # "us" -``` +``` #### upper -`upper` is a Function that converts a given string to uppercase. +`upper` is a Function that converts a given string to uppercase. ``` upper(string) ``` ``` upper("up") # "UP" -``` +``` #### values `values` returns a list of the map values, in the order of the sorted keys. This function only works on flat maps. ``` @@ -622,9 +660,9 @@ values(map) ``` values({"a" = 1,"b" = 2}) # [1, 2] -``` +``` #### yamldecode -`yamldecode` parses the given JSON string and, if it is valid, returns the value it represents. +`yamldecode` parses the given JSON string and, if it is valid, returns the value it represents. ``` yamldecode(string) ``` @@ -635,7 +673,7 @@ yamldecode("hello: world\narray: [1, 2, 3]") array = [1, 2, 3] hello = "world" } -``` +``` #### yamlencode `yamlencode` returns a JSON serialization of the given value. ``` @@ -650,7 +688,7 @@ yamlencode({array = [1, 2, 3], hello = "world"}) - 2 - 3 "hello": "world" -``` +``` #### zipmap `zipmap` constructs a map from a list of keys and a corresponding list of values. The lenght of each list must be equal @@ -664,4 +702,4 @@ zipmap(["key1", "key2"], ["val1", "val2"]) "key1" = "val1" "key2" = "val2" } -``` \ No newline at end of file +``` diff --git a/docs/index.md b/docs/index.md index 5c0c5370..c553226f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1306,7 +1306,7 @@ domain: "dev.example.com" values { domain = "overdev.example.com" env = "dev" - willBeOverriden = "override_me" + willBeOverridden = "override_me" } ``` @@ -1319,7 +1319,7 @@ values { ```yaml # secrets.yml (assuming this one has been encrypted) -willBeOverriden: overrided +willBeOverridden: overridden ``` ``` @@ -1339,7 +1339,7 @@ releases: values: domain: "{{ .Values.domain }}" # == "overdev.example.com" env: "{{ .Values.env }}" # == "local" - willBeOverriden: "{{ .Values.willBeOverriden }}" # == "overrided" + willBeOverridden: "{{ .Values.willBeOverridden }}" # == "overridden" ``` ## DAG-aware installation/deletion ordering with `needs` diff --git a/go.mod b/go.mod index 0ac95732..4b75696f 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.7.0 github.com/gosuri/uitable v0.0.4 + github.com/hashicorp/go-cty-funcs v0.1.0 github.com/hashicorp/go-getter/v2 v2.2.3 github.com/hashicorp/hcl/v2 v2.24.0 github.com/helmfile/chartify v0.26.2 @@ -145,6 +146,7 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/antchfx/jsonquery v1.3.6 // indirect github.com/antchfx/xpath v1.3.5 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect diff --git a/go.sum b/go.sum index 36da757c..b740434e 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/antchfx/jsonquery v1.3.6/go.mod h1:fGzSGJn9Y826Qd3pC8Wx45avuUwpkePsAC github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.3.5 h1:PqbXLC3TkfeZyakF5eeh3NTWEbYl4VHNVeufANzDbKQ= github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= @@ -445,6 +447,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-cty-funcs v0.1.0 h1:TRO/6x1unvTPpotTgrTU7qlcbd99JBLt+vmF6dMF6lY= +github.com/hashicorp/go-cty-funcs v0.1.0/go.mod h1:crc3afXAsjGOJ+12LNX8PImH+ejyxOjnjvsUteKcFIw= github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk= github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= diff --git a/pkg/hcllang/hcl_functions.go b/pkg/hcllang/hcl_functions.go index 83309739..a68f2815 100644 --- a/pkg/hcllang/hcl_functions.go +++ b/pkg/hcllang/hcl_functions.go @@ -2,6 +2,7 @@ package hcllang import ( "dario.cat/mergo" + "github.com/hashicorp/go-cty-funcs/cidr" "github.com/hashicorp/hcl/v2/ext/tryfunc" ctyyaml "github.com/zclconf/go-cty-yaml" "github.com/zclconf/go-cty/cty/function" @@ -14,6 +15,10 @@ func HCLFunctions(additionnalFunctions map[string]function.Function) (map[string "can": tryfunc.CanFunc, "ceil": stdlib.CeilFunc, "chomp": stdlib.ChompFunc, + "cidrhost": cidr.HostFunc, + "cidrnetmask": cidr.NetmaskFunc, + "cidrsubnet": cidr.SubnetFunc, + "cidrsubnets": cidr.SubnetsFunc, "coalesce": stdlib.CoalesceFunc, "coalescelist": stdlib.CoalesceListFunc, "compact": stdlib.CompactFunc, diff --git a/pkg/hcllang/hcl_loader_test.go b/pkg/hcllang/hcl_loader_test.go index dc0899c6..252d3e45 100644 --- a/pkg/hcllang/hcl_loader_test.go +++ b/pkg/hcllang/hcl_loader_test.go @@ -545,3 +545,26 @@ func TestHCL_ValuesOverride_TransitiveDependencies(t *testing.T) { t.Errorf("Expected full_path=override/path (overridden), got %v", actual["full_path"]) } } + +// TestHCL_CIDRFunctions tests that HCL CIDR functions work correctly +func TestHCL_CIDRFunctions(t *testing.T) { + l := newHCLLoader() + l.AddFiles([]string{"testdata/cidr.hcl"}) + + actual, err := l.HCLRender() + if err != nil { + t.Fatalf("Render error: %s", err.Error()) + } + + expected := map[string]any{ + "host": "10.0.0.2", + "host_neg": "10.255.255.254", + "netmask": "255.255.0.0", + "subnet": "10.2.0.0/16", + "subnets": []any{"10.0.0.0/12", "10.16.0.0/12"}, + } + + if diff := cmp.Diff(expected, actual); diff != "" { + t.Error(diff) + } +} diff --git a/pkg/hcllang/testdata/cidr.hcl b/pkg/hcllang/testdata/cidr.hcl new file mode 100644 index 00000000..7b3d1ee1 --- /dev/null +++ b/pkg/hcllang/testdata/cidr.hcl @@ -0,0 +1,7 @@ +values { + host = cidrhost("10.0.0.0/8", 2) + host_neg = cidrhost("10.0.0.0/8", -2) + netmask = cidrnetmask("10.0.0.0/16") + subnet = cidrsubnet("10.0.0.0/8", 8, 2) + subnets = cidrsubnets("10.0.0.0/8", 4, 4) +}