We use Terraformer
to acquire state and HCL
files, which we then utilize in Terraform
modules to manage our infrastructure and prevent drift. As tfvars
files are fed as variables to the module, we need tfvars
output to automate the entire process. Here is a simple illustration:
infrastructure -> terraformer -> (HCl, statefile) -> tfvars -> terraform module -> infrastructure
Since Terraformer
does not support outputting tfvars
, I am attempting to write a Go module to convert HCL
files (or statefile
) to .tfvars
files. Searching through the internet, I came across this Stack Overflow question and solution; however, it was about parsing a tfvars
file. The accepted answer suggested using the cty
module as a low-level parser.
Considering that, I tried the following:
// ...
parser := hclparse.NewParser()
// filePath point to a hclfile in JOSON format
hclFile, diags := parser.ParseJSONFile(filePath)
attrs, diags := hclFile.Body.JustAttributes()
if diags.HasErrors() {
log.Fatal(diags.Error())
}
vals := make(map[string]cty.Value, len(attrs))
for name, attr := range attrs {
vals[name], diags = attr.Expr.Value(nil)
if diags.HasErrors() {
log.Fatal(diags.Error())
}
}
Up until now, I managed to parse the HCL to cty types. However, as cty does not support serializing to a tfvars file, I tried writing my own serializer. Is this a correct workaround? How could I achieve this goal?
Here is the serializer:
func serializeValue(value cty.Value) string {
switch {
case value.Type().IsPrimitiveType():
switch value.Type() {
case cty.String:
return fmt.Sprintf("\"%s\"", value.AsString())
case cty.Number:
return value.AsBigFloat().Text('f', -1)
case cty.Bool:
return fmt.Sprintf("%t", value.True())
}
case value.Type().IsListType() || value.Type().IsTupleType():
return serializeList(value)
case value.Type().IsMapType() || value.Type().IsObjectType():
return serializeMapOrObject(value)
default:
panic("Unhandled type")
}
return ""
}
func serializeMapOrObject(value cty.Value) string {
var elements []string
for key, val := range value.AsValueMap() {
elements = append(elements, fmt.Sprintf("\"%s\" = %s\n", key, serializeValue(val)))
}
return fmt.Sprintf("{%s}", strings.Join(elements, "\n"))
}
func serializeList(value cty.Value) string {
var elements []string
for _, elem := range value.AsValueSlice() {
elements = append(elements, serializeValue(elem))
}
return fmt.Sprintf("[%s]", strings.Join(elements, ", "))
}
If you have a cty.Value
that you know is of an object or map type and that all of the attribute/key names are valid HCL identifiers then you can use HCL's hclwrite
package to generate something that Terraform would accept as the content a .tfvars
file.
Terraform's .tfvars
file format is a relatively-simple application of HCL where the root body is interpreted as "just attributes" (in HCL's terminology) and then each attribute has its expression evaluated with no variables or functions available.
You can generate such a file like this, with a suitable cty.Value
stored in variable obj
:
f := hclwrite.NewEmptyFile()
body := f.Body()
for it := obj.ElementIterator(); it.Next(); {
k, v := it.Element()
name := k.AsString()
body.SetAttributeValue(name, v)
}
result := f.Bytes()
After executing the above, result
is a []byte
containing the content you could write to your .tfvars
file.
cty
does not iterate map elements or object attributes in any predictable order, so the attributes in the result will also be in an unpredictable order. If you want to guarantee the order then you'll need to do something a little more elaborate than this, but I'll leave that as an exercise since the above shows the basic mechanic of generating a HCL file that contains only attributes whose values are constants.