Go template list variable to replace from template - go

So I'm using go template and I would like to be able to generate a configmap dynamically by getting the list of the variable from the template.
To be precise, let's say I have a file name test.yaml.tmpl that looks something like that:
car:
color: {{ .color }}
model: {{ .model }}
I would like to be able to generate an array in my go code containing [color model] or [.color .model] , I can work with the dots myself.
I looked at the differents options but I could not get the result I want, the closest I got was using template.Root.Nodes to extract the variable value but it's not perfect and may generate unwanted error in specific conditions.
Does someone has a clean way to generate an array of the template variable?
EDIT 1
I tried to use the tree, and that gives me this type of output:
&{test.yaml.tmpl test.yaml.tmpl ---
car:
color: {{ .color }}
model: {{ .model }}
0 ---
car:
color: {{ .color }}
model: {{ .model }}
[] <nil> [{8 995 45} {11 409 {{ 22} {0 0 0}] 1 [] map[] 0 0}
The issue is that I don't get to access the fields node the only methods available when accessing the tree are:
ErrorContext
Parse
Copy and it's methods
Mode
Name
ParseName
Root and the NodeType method
Still can't get the list of fields.
EDIT 2
When printing the tree.Root.Nodes I get the full yaml output with the variables to replace like that:
(*parse.ActionNode)(0xc00007f1d0)({{ .color }}),
(*parse.TextNode)(0xc00007f200)

Well you can user regex you know. ;P
package main
import (
"fmt"
"regexp"
)
var fieldSearch = regexp.MustCompile(`{{ \..* }}`)
func main() {
template := `
car:
color: {{ .color }}
model: {{ .model }}
`
res := fieldSearch.FindAll([]byte(template), -1)
for i := range res {
// this is just for readability you can replace this with
// magic numbers if you want
res[i] = res[i][len("{{ .") : len(res[i])-len(" }}")]
}
for _, v := range res {
fmt.Println(string(v))
}
}

Ok, so I used the link posted by #icza How to get a map or list of template 'actions' from a parsed template?and treated the returned string to get what I need, this resulted in the following:
func main() {
node := listNodeFields(t.Tree.Root)
var tNode []string
for _, n := range node {
r := n[len("{{.* .") : len(n)-len(" .*}}")]
if strings.Contains(n, "or ") {
r = r + " or"
}
tNode = append(tNode, r)
}
config := make(map[string]string)
for _, n := range tNode {
var value string
var present bool
if strings.Contains(n, " or") {
n = strings.Replace(n, " or", "", -1)
value, present = os.LookupEnv(n)
if !present {
fmt.Println("The variable " + value + " is not set but there is a default value in template " + t.Name() + "!")
}
} else {
value, present = os.LookupEnv(n)
if !present {
return nil, errors.New("The variable " + f + " is not set but exists in the template " + t.Name() + "!")
}
}
config[n] = value
}
}
func listNodeFields(node parse.Node) []string {
var res []string
if node.Type() == parse.NodeAction {
res = append(res, node.String())
}
if ln, ok := node.(*parse.ListNode); ok {
for _, n := range ln.Nodes {
res = append(res, listNodeFields(n)...)
}
}
return res
}
As I don't want to generate errors in case I'm using default value I added a specific treatment to not get errors from the missingkey=error option of the template.

Related

Get the type of value using cty in hclwrite

am looking for a way to find the type of variable using go-cty package in hclwrite.
My aim is to generate a variables file like below
variable "test_var" {
val1 = bool
val2 = string
val3 = number
}
reference: https://developer.hashicorp.com/terraform/language/values/variables
I am using the below code to generate this.
vars := hclwrite.NewEmptyFile()
vars_root_body := vars.Body()
vars_file, vars_create_err := os.Create("variables.tf")
logErrors(vars_create_err)
vars_block := vars_root_body.AppendNewBlock("variable",[]string{"test_var"})
vars_block_body := vars_block.Body()
vars_block_body.SetAttributeValue("val", cty.Value{})
_, vars_write_err := vars_file.Write(vars.Bytes())
logErrors(vars_write_err)
defer vars_file.Close()
the above code generates this
variable "test_var" {
val = null
}
I want to fetch the type of that variable and set the attribute value based on that type, as show in the reference link above. I tried lot of ways but didn't get anything. Can someone please help me on this?
I tried the above code and lot of other ways like
cty.SetValEmpty(cty.Bool)
but it didn't work.
The expected syntax for a variable block in Terraform includes an argument named type, not an argument named val. From your example I assume that you are intending to populate type.
The type constraint syntax that Terraform uses is not directly part of HCL and so there isn't any built-in way to generate that syntax in only one step. However, type constraint are built from HCL's identifier and function call syntaxes, and hclwrite does have some functions for helping to generate those as individual parts:
TokensForIdentifier
TokensForFunctionCall
f := hclwrite.NewEmptyFile()
rootBody := f.Body()
varBlock := rootBody.AppendNewBlock("variable", []string{"example"})
varBody := varBlock.Body()
varBody.SetAttributeRaw(
"type",
hclwrite.TokensForFunctionCall(
"set",
hclwrite.TokensForIdentifier("string"),
),
)
fmt.Printf("%s", f.Bytes())
The above will generate the following:
variable "example" {
type = set(string)
}
If you already have a cty.Value value then you can obtain its type using the Type method. However, as mentioned above there isn't any ready-to-use function for converting a type into a type expression, so if you want to be able to generate a type constraint for any value then you'd need to write a function for this yourself, wrapping the TokensForFunctionCall and TokensForIdentifier functions. For example:
package main
import (
"fmt"
"sort"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
)
func main() {
f := hclwrite.NewEmptyFile()
rootBody := f.Body()
varBlock := rootBody.AppendNewBlock("variable", []string{"example"})
varBody := varBlock.Body()
varBody.SetAttributeRaw(
"type",
typeExprTokens(cty.Set(cty.String)),
)
fmt.Printf("%s", f.Bytes())
}
func typeExprTokens(ty cty.Type) hclwrite.Tokens {
switch ty {
case cty.String:
return hclwrite.TokensForIdentifier("string")
case cty.Bool:
return hclwrite.TokensForIdentifier("bool")
case cty.Number:
return hclwrite.TokensForIdentifier("number")
case cty.DynamicPseudoType:
return hclwrite.TokensForIdentifier("any")
}
if ty.IsCollectionType() {
etyTokens := typeExprTokens(ty.ElementType())
switch {
case ty.IsListType():
return hclwrite.TokensForFunctionCall("list", etyTokens)
case ty.IsSetType():
return hclwrite.TokensForFunctionCall("set", etyTokens)
case ty.IsMapType():
return hclwrite.TokensForFunctionCall("map", etyTokens)
default:
// Should never happen because the above is exhaustive
panic("unsupported collection type")
}
}
if ty.IsObjectType() {
atys := ty.AttributeTypes()
names := make([]string, 0, len(atys))
for name := range atys {
names = append(names, name)
}
sort.Strings(names)
items := make([]hclwrite.ObjectAttrTokens, len(names))
for i, name := range names {
items[i] = hclwrite.ObjectAttrTokens{
Name: hclwrite.TokensForIdentifier(name),
Value: typeExprTokens(atys[name]),
}
}
return hclwrite.TokensForObject(items)
}
if ty.IsTupleType() {
etys := ty.TupleElementTypes()
items := make([]hclwrite.Tokens, len(etys))
for i, ety := range etys {
items[i] = typeExprTokens(ety)
}
return hclwrite.TokensForTuple(items)
}
panic(fmt.Errorf("unsupported type %#v", ty))
}
This program will generate the same output as the previous example. You can change func main to pass a different type to typeExprTokens to see how it behaves with some different types.

Is it possible to assign to a regular variable and slice in the same statement?

I'm making a chess game and I want to do a series of type assertions in the same var statement, then pass them to a function that handles it, but apparently, Go doesn't allow me to assign to a regular variable and a slice index in the same statement:
// inside a function:
asserts := make([]bool, 0, 10)
assertionHandler := func(ok *[]bool) {
for _, b := range *ok {
if !b {
msg := "pieceCliked: failed while trying to do type assertion\n%s\n\n"
utils.LogPrintError(errors.New(fmt.Sprintf(msg, string(debug.Stack()))))
}
}
*ok = make([]bool, 0, 10)
}
var (
possibleSquares []string
// The following results in a syntax error: expected type, found '='
dataObject, asserts[0] = data.(map[string]any)
playerData, asserts[1] = dataObject["playerData"].(map[string]any)
square, asserts[2] = playerData["selectedPieceLocation"].(string)
piece, asserts[3] = playerData["selectedPiece"].(string)
color, asserts[4] = playerData["selectedPieceColor"].(string)
)
assertionHandler(asserts)
Is it possible to do what I'm trying to do?
Not the way you're doing it, no. A var block defines new variables and their types, but you're trying to assign to both new variables with no types (hence the error expected type) and elements of an existing slice within that block.
You could do:
var (
possibleSquares []string
dataObject map[string]any
playerData map[string]any
square string
piece string
color string
)
dataObject, asserts[0] = data.(map[string]any)
playerData, asserts[1] = dataObject["playerData"].(map[string]any)
square, asserts[2] = playerData["selectedPieceLocation"].(string)
piece, asserts[3] = playerData["selectedPiece"].(string)
color, asserts[4] = playerData["selectedPieceColor"].(string)
Another answer describes why the code in the question does not work. Here's another workaround:
Write assertion handler to use variadic argument:
func assertionHandler(asserts ...bool) bool {
result := true
for _, b := range assserts {
if !b {
result = false
msg := "pieceCliked: failed while trying to do type assertion\n%s\n\n"
utils.LogPrintError(errors.New(fmt.Sprintf(msg, string(debug.Stack()))))
}
}
return result
}
Use short variable declarations to collect the values and bool results:
dataObject, assert0 := data.(map[string]any)
playerData, assert1 := dataObject["playerData"].(map[string]any)
square, assert2 := playerData["selectedPieceLocation"].(string)
piece, assert3 := playerData["selectedPiece"].(string)
color, assert4 := playerData["selectedPieceColor"].(string)
if !assertionHandler(assert0, assert1, assert2, assert3, assert4) {
return
}

Creating Map From Changelog With Path in Slice of Strings (Maybe append to map!?)

I have a slice of changes that looks like this:
type Change struct {
Type string // The type of change detected; can be one of create, update or delete
Path []string // The path of the detected change; will contain any field name or array index that was part of the traversal
From interface{} // The original value that was present in the "from" structure
To interface{} // The new value that was detected as a change in the "to" structure
}
I'm trying to take this and create a new map[string]interface{} from this.
I realized looping through path I'd need to build upon the prior path but I'm unsure how to do this. E.g. Path with this: {"one": {"two": {"three": "value"}}} would have []string{"one", "two", "three"}
Right now I have this code:
for _, change := range changeLog {
for i, _ := range change.Path {
changeMap[change.Path[i]] = make(map[string]interface{})
if i == len(change.Path)-1 {
if castMap, ok := change.To.(map[string]interface{}); ok {
changeMap[p] = castMap
}
}
}
}
But this doesn't account for the parents hence the result is a flat structure. How do I achieve what I'm trying to achieve.
If you want to transverse your slice of Change to a tree struct based on maps, you should use some node struct to hold the tree data, because a variable cannot hold a map and an interface (the To value) at the same time. I make this sample that works and build you a tree based on your changelog slice.
package main
import (
"fmt"
"strings"
)
type Change struct {
Type string // The type of change detected; can be one of create, update or delete
Path []string // The path of the detected change; will contain any field name or array index that was part of the traversal
From interface{} // The original value that was present in the "from" structure
To interface{} // The new value that was detected as a change in the "to" structure
}
type Node struct {
Children map[string]*Node
To interface{}
}
//Helper for printing the tree
func (n Node) Print(ident int) {
id := strings.Repeat(" ", ident)
fmt.Println(id+"To:", n.To)
for key, value := range n.Children {
fmt.Println(id + "[" + key + "]")
value.Print(ident + 2)
}
}
func main() {
changeLog := make([]Change, 0)
changeLog = append(changeLog, Change{
Type: "update",
Path: []string{"a", "b"},
From: 1,
To: 2,
})
changeLog = append(changeLog, Change{
Type: "update",
Path: []string{"a", "c"},
From: 3,
To: 4,
})
var node *Node
root := &Node{
Children: make(map[string]*Node),
To: nil,
}
for _, change := range changeLog {
node = root
for _, p := range change.Path {
if _, found := node.Children[p]; !found {
node.Children[p] = &Node{
Children: make(map[string]*Node),
To: nil,
}
}
node = node.Children[p]
}
node.To = change.To
}
//Note: if you try to get a non-exist path this way, the following will panic!
//You should make a func to get the value of a path or get nil back if path not exists.
fmt.Println(root.Children["a"].Children["b"].To)
fmt.Println(root.Children["a"].Children["c"].To)
root.Print(0)
}
Try it on goplay.space
Output:
To: <nil>
[a]
To: <nil>
[b]
To: 2
[c]
To: 4

Why can't I key in a string in Golang map?

I'm writing a function in go to remove duplicate characters in a string. Here is my approach. When I run the following test, why do I get this error? I'm new to Go and used to more dynamic languages like Ruby/Python.
panic: assignment to entry in nil map [recovered]
panic: assignment to entry in nil map
source.go
func removeDuplicate(s string) string {
var m map[string]int
var c_string []string = strings.Split(s, "")
for i :=0; i < len(c_string); i++ {
m[c_string[i]] = 0
}
for i :=0; i < len(c_string); i++ {
m[c_string[i]] = m[c_string[i]] + 1
}
var (
result string = ""
)
for i :=0; i < len(c_string); i++ {
if m[c_string[i]] < 1 {
result = result + c_string[i]
}
}
return result
}
source_test.go
func TestRemoveDuplicateChars(t *testing.T) {
got := removeDuplicateChars("abbcde")
if got != "abcde" {
t.Fatalf("removeDuplicateChars fails")
}
}
Because you haven't actually initilize/allocated m, you've only declared it. Make this; var m map[string]int into m := map[string]int{}.
Which does initilization and assignment both in the same statement. You could also add another line m = make(map[string]int) which would prevent the error though I personally prefer the compacted syntax.
fyi your code is barfing on this line; m[c_string[i]] = 0, the error message should make sense when combining that with the information above.

In Go how can I iterate over a pointer list?

I want to iterate over this :
*list.List
How can I do it?
func Foo(key string, values *list.List) string {
...
//I want to iterate and get value from "values"
}
this Foo is being called like this:
kvs := make(map[string]*list.List)
res := Foo(k, kvs[k]) //k is string
Thanks!
Check the example # http://golang.org/pkg/container/list/ :
func Foo(key string, vl *list.List) string {
for e := l.Front(); e != nil; e = e.Next() {
v := e.Value
}
return ""
}
//edit : check Create a Golang map of Lists

Resources