goterraformhcl

How to convert (write) hcl files as tfvars?


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, ", "))
}

Solution

  • 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.