Unable to read terraform variables.tf files into may go program - go

I am attempting to write a go program that reads in a terraform variables.tf and populates a struct for later manipulation. However, I am getting errors when attempting to "parse" the file. I Am hoping someone can tell me what I am doing wrong:
Code:
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
type Config struct {
Upstreams []*TfVariable `hcl:"variable,block"`
}
type TfVariable struct {
Name string `hcl:",label"`
// Default string `hcl:"default,optional"`
Type string `hcl:"type"`
Description string `hcl:"description,attr"`
// validation block
Sensitive bool `hcl:"sensitive,optional"`
}
func main() {
readHCLFile("examples/string.tf")
}
// Exits program by sending error message to standard error and specified error code.
func abort(errorMessage string, exitcode int) {
fmt.Fprintln(os.Stderr, errorMessage)
os.Exit(exitcode)
}
func readHCLFile(filePath string) {
content, err := ioutil.ReadFile(filePath)
if err != nil {
log.Fatal(err)
}
fmt.Printf("File contents: %s", content) // TODO: Remove me
file, diags := hclsyntax.ParseConfig(content, filePath, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
log.Fatal(fmt.Errorf("ParseConfig: %w", diags))
}
c := &Config{}
diags = gohcl.DecodeBody(file.Body, nil, c)
if diags.HasErrors() {
log.Fatal(fmt.Errorf("DecodeBody: %w", diags))
}
fmt.Println(c) // TODO: Remove me
}
ERROR
File contents: variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
sensitive = false
}
variable "other_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
sensitive = true
}
2021/03/13 19:55:49 DecodeBody: examples/string.tf:2,17-23: Variables not allowed; Variables may not be used here., and 3 other diagnostic(s)
exit status 1
Stack driver question is sadly for hcl1
Blog post I am referencing.

It looks like it's a bug/feature of the library, since as soon as you change string to "string", e.g.,
variable "image_id" {
type = string
...
to
variable "image_id" {
type = "string"
...
gohcl.DecodeBody succeeds.
--- UPDATE ---
So, they do use this package in Terraform, BUT they custom-parse configs, i.e., they don't use gohcl.DecodeBody. They also custom-treat type attributes by using hcl.ExprAsKeyword (compare with description). As you assumed, they do use a custom type for type, but with custom parsing you don't have to.
Below is a working example:
package main
import (
"fmt"
"log"
"os"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
var (
configFileSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "variable",
LabelNames: []string{"name"},
},
},
}
variableBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "description",
},
{
Name: "type",
},
{
Name: "sensitive",
},
},
}
)
type Config struct {
Variables []*Variable
}
type Variable struct {
Name string
Description string
Type string
Sensitive bool
}
func main() {
config := configFromFile("examples/string.tf")
for _, v := range config.Variables {
fmt.Printf("%+v\n", v)
}
}
func configFromFile(filePath string) *Config {
content, err := os.ReadFile(filePath) // go 1.16
if err != nil {
log.Fatal(err)
}
file, diags := hclsyntax.ParseConfig(content, filePath, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
log.Fatal("ParseConfig", diags)
}
bodyCont, diags := file.Body.Content(configFileSchema)
if diags.HasErrors() {
log.Fatal("file content", diags)
}
res := &Config{}
for _, block := range bodyCont.Blocks {
v := &Variable{
Name: block.Labels[0],
}
blockCont, diags := block.Body.Content(variableBlockSchema)
if diags.HasErrors() {
log.Fatal("block content", diags)
}
if attr, exists := blockCont.Attributes["description"]; exists {
diags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description)
if diags.HasErrors() {
log.Fatal("description attr", diags)
}
}
if attr, exists := blockCont.Attributes["sensitive"]; exists {
diags := gohcl.DecodeExpression(attr.Expr, nil, &v.Sensitive)
if diags.HasErrors() {
log.Fatal("sensitive attr", diags)
}
}
if attr, exists := blockCont.Attributes["type"]; exists {
v.Type = hcl.ExprAsKeyword(attr.Expr)
if v.Type == "" {
log.Fatal("type attr", "invalid value")
}
}
res.Variables = append(res.Variables, v)
}
return res
}
Add for completeness, example/string.tf:
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
sensitive = false
}
variable "other_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
sensitive = true
}

Since the Terraform language makes extensive use of various HCL features that require custom programming with the low-level HCL API, the Terraform team maintains a Go library terraform-config-inspect which understands the Terraform language enough to extract static metadata about top-level objects, including variables. It also deals with the fact that Terraform allows variable definitions in any .tf or .tf.json file interleaved with other declarations; putting them in variables.tf is only a convention.
For example:
mod, diags := tfconfig.LoadModule("examples")
if diags.HasErrors() {
log.Fatalf(diags.Error())
}
for _, variable := range mod.Variables {
fmt.Printf("%#v\n", variable)
}
This library is the same code used by Terraform Registry to produce the documentation about module input variables, so it supports all Terraform language versions that the Terraform Registry does (at the time of writing, going back to the Terraform v0.10 language, since that's the first version that can install modules from a registry) and supports both the HCL native syntax and JSON representations of the Terraform language.

Related

Unmarshal yaml map dict key to struct property

I really searched a while here, but didn't found an adequate answer:
I am trying to unmarshall yaml dict keys onto a property of a struct rather than the key of a map.
Given this yaml
commands:
php:
service: php
bin: /bin/php
node:
service: node
bin: /bin/node
I am able to unmarshall this into a struct like this:
type Config struct {
Commands map[string]struct {
Service string
Bin string
}
}
But how am I able to unmarshall it into a struct like this:
type Config struct {
Commands []struct {
Name string // <-- this should be key from the yaml (i.e. php or node)
Service string
Bin string
}
}
Thx in advance for the help
You can write a custom unmarshaler, like this (on Go playground):
package main
import (
"fmt"
"gopkg.in/yaml.v3"
)
var input []byte = []byte(`
commands:
php:
service: php
bin: /bin/php
node:
service: node
bin: /bin/node
`)
type Command struct {
Service string
Bin string
}
type NamedCommand struct {
Command
Name string
}
type NamedCommands []NamedCommand
type Config struct {
Commands NamedCommands
}
func (p *NamedCommands) UnmarshalYAML(value *yaml.Node) error {
if value.Kind != yaml.MappingNode {
return fmt.Errorf("`commands` must contain YAML mapping, has %v", value.Kind)
}
*p = make([]NamedCommand, len(value.Content)/2)
for i := 0; i < len(value.Content); i += 2 {
var res = &(*p)[i/2]
if err := value.Content[i].Decode(&res.Name); err != nil {
return err
}
if err := value.Content[i+1].Decode(&res.Command); err != nil {
return err
}
}
return nil
}
func main() {
var f Config
var err error
if err = yaml.Unmarshal(input, &f); err != nil {
panic(err)
}
for _, cmd := range f.Commands {
fmt.Printf("%+v\n", cmd)
}
}
I have split the command data into Command and NamedCommand to make the code simpler, since you can just call Decode giving the embedded Command struct for the values. If everything was in the same struct, you'd need to manually map keys to struct fields.

Why does this TOML table not parse correctly in Go

I'm relatively new to Golang. I've been trying to solve this issue for a little while but have not found anything to help me. I am just trying to parse a config.toml file into my application.
My config.toml file is as follows:
[[trees]]
name = "test1"
tags = ["main", "dev"]
[[trees]]
name = "test2"
tags = ["main", "dev", "prod"]
And the code which I am using to read the file:
import (
"os"
"fmt"
toml "github.com/pelletier/go-toml/v2"
)
type Configuration struct {
Trees []tree `toml:"trees"`
}
type tree struct {
name string `toml:"name"`
tags []string `toml:"tags"`
}
func main() Configuration {
doc, e := os.ReadFile("config.toml")
if e != nil {
panic(e)
}
var cfg Configuration
err := toml.Unmarshal(doc, &cfg)
if err != nil {
panic(err)
}
fmt.Println(cfg.Trees)
return cfg
}
When I execute the code, I get the following empty array as an output:
> [{ []} { []}]
If anyone can tell me what I am doing wrong here, that would be much appreciated.
You need to capitalize the struct field names:
type Configuration struct {
Trees []tree `toml:"trees"`
}
type tree struct {
Name string `toml:"name"`
Tags []string `toml:"tags"`
}
Here's a related explanation for json.Marshal: the structure fields need to be public (ie, uppercase names).

HCL Decoding: Blocks with multiple labels

My goal is to parse a HCL configuration (Terraform Configuration) and then write the collected data about variables, outputs, resources blocks and data blocks into a Markdown file.
Variables and outputs are no problem, however, as soon as I trying to decode resource blocks, which have multiple labels.
Works:
variable "foo" {
type = "bar"
}
Doesn't Work:
resource "foo" "bar" {
name = "biz"
}
Error: Extraneous label for resource; Only 1 labels (name) are expected for resource blocks.
Type declaration Code:
import (
"log"
"os"
"strconv"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
)
type Variable struct {
Name string `hcl:",label"`
Description string `hcl:"description,optional"`
Sensitive bool `hcl:"sensitive,optional"`
Type *hcl.Attribute `hcl:"type,optional"`
Default *hcl.Attribute `hcl:"default,optional"`
Options hcl.Body `hcl:",remain"`
}
type Output struct {
Name string `hcl:",label"`
Description string `hcl:"description,optional"`
Sensitive bool `hcl:"sensitive,optional"`
Value string `hcl:"value,optional"`
Options hcl.Body `hcl:",remain"`
}
type Resource struct {
Name string `hcl:"name,label"`
Options hcl.Body `hcl:",remain"`
}
type Data struct {
Name string `hcl:"name,label"`
Options hcl.Body `hcl:",remain"`
}
type Config struct {
Outputs []*Output `hcl:"output,block"`
Variables []*Variable `hcl:"variable,block"`
Resources []*Resource `hcl:"resource,block"`
Data []*Data `hcl:"data,block"`
}
Decoding Code:
func createDocs(hclPath string) map[string][]map[string]string {
var variables, outputs []map[string]string
parsedConfig := make(map[string][]map[string]string)
hclConfig := make(map[string][]byte)
c := &Config{}
// Iterate all Terraform files and safe the contents in the hclConfig map
for _, file := range filesInDirectory(hclPath, ".tf") {
fileContent, err := os.ReadFile(hclPath + "/" + file.Name())
if err != nil {
log.Fatal(err)
}
hclConfig[file.Name()] = fileContent
}
// Iterate all file contents
for k, v := range hclConfig {
parsedConfig, diags := hclsyntax.ParseConfig(v, k, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
log.Fatal(diags)
}
diags = gohcl.DecodeBody(parsedConfig.Body, nil, c)
if diags.HasErrors() {
log.Fatal(diags)
}
}
for _, v := range c.Variables {
var variableType string
var variableDefault string
if v.Type != nil {
variableType = (v.Type.Expr).Variables()[0].RootName()
}
if v.Default != nil {
variableDefault = (v.Default.Expr).Variables()[0].RootName()
}
variables = append(variables, map[string]string{"name": v.Name, "description": v.Description,
"sensitive": strconv.FormatBool(v.Sensitive), "type": variableType, "default": variableDefault})
}
for _, v := range c.Outputs {
outputs = append(outputs, map[string]string{"name": v.Name, "description": v.Description,
"sensitive": strconv.FormatBool(v.Sensitive), "value": v.Value})
}
parsedConfig["variables"], parsedConfig["outputs"] = variables, outputs
return parsedConfig
}
Question: How can I parse multiple labels from resource blocks?
The error you shared is due to the definition of type Resource. resource blocks (and data blocks) in Terraform expect two labels, indicating the resource type and name. To match that in the schema you're implying with these struct types, you'll need to define to fields that are tagged as label:
type Resource struct {
Type string `hcl:"type,label"`
Name string `hcl:"name,label"`
Options hcl.Body `hcl:",remain"`
}
type Data struct {
Type string `hcl:"type,label"`
Name string `hcl:"name,label"`
Options hcl.Body `hcl:",remain"`
}
Although this should work for the limited input you showed here, I want to caution that you are using the higher-level gohcl API which can decode only a subset of HCL that maps well onto Go's struct types. Terraform itself uses the lower-level APIs of hcl.Body and hcl.Expression directly, which allows the Terraform language to include some HCL features that the gohcl API cannot directly represent.
Depending on what your goal is, you may find it better to use the official library terraform-config-inspect, which can parse, decode, and describe a subset of the Terraform language at a higher level of abstraction than the HCL API itself. It also supports modules written for Terraform versions going all the way back to Terraform v0.11, and is the implementation that backs the analysis of modules done by Terraform Registry.

Unmarshal map[string]DynamoDBAttributeValue into a struct

I'm trying to set-up an AWS-lambda using aws-sdk-go that is triggered whenever a new user is added to a certain dynamodb table.
Everything is working just fine but I can't find a way to unmarshal a map map[string]DynamoDBAttributeValue like:
{
"name": {
"S" : "John"
},
"residence_address": {
"M": {
"address": {
"S": "some place"
}
}
}
}
To a given struct, for instance, a User struct. Here is shown an example of unsmarhaling a map[string]*dynamodb.AttributeValue into a given interface, but I can't find a way to do the same thing with map[string]DynamoDBAttributeValue even though these types seem to fit the same purposes.
map[string]DynamoDBAttributeValue is returned by a events.DynamoDBEvents from package github.com/aws/aws-lambda-go/events. This is my code:
package handler
import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
func HandleDynamoDBRequest(ctx context.Context, e events.DynamoDBEvent) {
for _, record := range e.Records {
if record.EventName == "INSERT" {
// User Struct
var dynamoUser model.DynamoDBUser
// Of course this can't be done for incompatible types
_ := dynamodbattribute.UnmarshalMap(record.Change.NewImage, &dynamoUser)
}
}
}
Of course, I can marshal record.Change.NewImage to JSON and unmarshal it back to a given struct, but then, I would have to manually initialize dynamoUser attributes starting from the latter ones.
Or I could even write a function that parses map[string]DynamoDBAttributeValue to map[string]*dynamodb.AttributeValue like:
func getAttributeValueMapFromDynamoDBStreamRecord(e events.DynamoDBStreamRecord) map[string]*dynamodb.AttributeValue {
image := e.NewImage
m := make(map[string]*dynamodb.AttributeValue)
for k, v := range image {
if v.DataType() == events.DataTypeString {
s := v.String()
m[k] = &dynamodb.AttributeValue{
S : &s,
}
}
if v.DataType() == events.DataTypeBoolean {
b := v.Boolean()
m[k] = &dynamodb.AttributeValue{
BOOL : &b,
}
}
// . . .
if v.DataType() == events.DataTypeMap {
// ?
}
}
return m
}
And then simply use dynamodbattribute.UnmarshalMap, but on events.DataTypeMap it would be quite a tricky process.
Is there a way through which I can unmarshal a DynamoDB record coming from a events.DynamoDBEvent into a struct with a similar method shown for map[string]*dynamodb.AttributeValue?
I tried the function you provided, and I met some problems with events.DataTypeList, so I managed to write the following function that does the trick:
// UnmarshalStreamImage converts events.DynamoDBAttributeValue to struct
func UnmarshalStreamImage(attribute map[string]events.DynamoDBAttributeValue, out interface{}) error {
dbAttrMap := make(map[string]*dynamodb.AttributeValue)
for k, v := range attribute {
var dbAttr dynamodb.AttributeValue
bytes, marshalErr := v.MarshalJSON(); if marshalErr != nil {
return marshalErr
}
json.Unmarshal(bytes, &dbAttr)
dbAttrMap[k] = &dbAttr
}
return dynamodbattribute.UnmarshalMap(dbAttrMap, out)
}
I was frustrated that the type of NewImage from the record wasn't map[string]*dynamodb.AttributeValue so I could use the dynamodbattribute package.
The JSON representation of events.DynamoDBAttributeValue seems to be the same as the JSON represenation of dynamodb.AttributeValue.
So I tried creating my own DynamoDBEvent type and changed the type of OldImage and NewImage, so it would be marshalled into map[string]*dynamodb.AttributeValue instead of map[string]events.DynamoDBAttributeValue
It is a little bit ugly but it works for me.
package main
import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"fmt"
)
func main() {
lambda.Start(lambdaHandler)
}
// changed type of event from: events.DynamoDBEvent to DynamoDBEvent (see below)
func lambdaHandler(event DynamoDBEvent) error {
for _, record := range event.Records {
change := record.Change
newImage := change.NewImage // now of type: map[string]*dynamodb.AttributeValue
var item IdOnly
err := dynamodbattribute.UnmarshalMap(newImage, &item)
if err != nil {
return err
}
fmt.Println(item.Id)
}
return nil
}
type IdOnly struct {
Id string `json:"id"`
}
type DynamoDBEvent struct {
Records []DynamoDBEventRecord `json:"Records"`
}
type DynamoDBEventRecord struct {
AWSRegion string `json:"awsRegion"`
Change DynamoDBStreamRecord `json:"dynamodb"`
EventID string `json:"eventID"`
EventName string `json:"eventName"`
EventSource string `json:"eventSource"`
EventVersion string `json:"eventVersion"`
EventSourceArn string `json:"eventSourceARN"`
UserIdentity *events.DynamoDBUserIdentity `json:"userIdentity,omitempty"`
}
type DynamoDBStreamRecord struct {
ApproximateCreationDateTime events.SecondsEpochTime `json:"ApproximateCreationDateTime,omitempty"`
// changed to map[string]*dynamodb.AttributeValue
Keys map[string]*dynamodb.AttributeValue `json:"Keys,omitempty"`
// changed to map[string]*dynamodb.AttributeValue
NewImage map[string]*dynamodb.AttributeValue `json:"NewImage,omitempty"`
// changed to map[string]*dynamodb.AttributeValue
OldImage map[string]*dynamodb.AttributeValue `json:"OldImage,omitempty"`
SequenceNumber string `json:"SequenceNumber"`
SizeBytes int64 `json:"SizeBytes"`
StreamViewType string `json:"StreamViewType"`
}
I have found the same problem and the solution is to perform a simple conversion of types. This is possible because in the end the type received by lambda events events.DynamoDBAttributeValue and the type used by the SDK V2 of AWS DynamoDB types.AttributeValue are the same. Next I show you the conversion code.
package aws_lambda
import (
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func UnmarshalDynamoEventsMap(
record map[string]events.DynamoDBAttributeValue, out interface{}) error {
asTypesMap := DynamoDbEventsMapToTypesMap(record)
err := attributevalue.UnmarshalMap(asTypesMap, out)
if err != nil {
return err
}
return nil
}
func DynamoDbEventsMapToTypesMap(
record map[string]events.DynamoDBAttributeValue) map[string]types.AttributeValue {
resultMap := make(map[string]types.AttributeValue)
for key, rec := range record {
resultMap[key] = DynamoDbEventsToTypes(rec)
}
return resultMap
}
// DynamoDbEventsToTypes relates the dynamo event received by AWS Lambda with the data type that is
// used in the Amazon SDK V2 to deal with DynamoDB data.
// This function is necessary because Amazon does not provide any kind of solution to make this
// relationship between types of data.
func DynamoDbEventsToTypes(record events.DynamoDBAttributeValue) types.AttributeValue {
var val types.AttributeValue
switch record.DataType() {
case events.DataTypeBinary:
val = &types.AttributeValueMemberB{
Value: record.Binary(),
}
case events.DataTypeBinarySet:
val = &types.AttributeValueMemberBS{
Value: record.BinarySet(),
}
case events.DataTypeBoolean:
val = &types.AttributeValueMemberBOOL{
Value: record.Boolean(),
}
case events.DataTypeList:
var items []types.AttributeValue
for _, value := range record.List() {
items = append(items, DynamoDbEventsToTypes(value))
}
val = &types.AttributeValueMemberL{
Value: items,
}
case events.DataTypeMap:
items := make(map[string]types.AttributeValue)
for k, v := range record.Map() {
items[k] = DynamoDbEventsToTypes(v)
}
val = &types.AttributeValueMemberM{
Value: items,
}
case events.DataTypeNull:
val = nil
case events.DataTypeNumber:
val = &types.AttributeValueMemberN{
Value: record.Number(),
}
case events.DataTypeNumberSet:
val = &types.AttributeValueMemberNS{
Value: record.NumberSet(),
}
case events.DataTypeString:
val = &types.AttributeValueMemberS{
Value: record.String(),
}
case events.DataTypeStringSet:
val = &types.AttributeValueMemberSS{
Value: record.StringSet(),
}
}
return val
}
There is a package that allows conversion from events.DynamoDBAttributeValue to dynamodb.AttributeValue
https://pkg.go.dev/github.com/aereal/go-dynamodb-attribute-conversions/v2
From there one can unmarshal AttributeValue into struct
func Unmarshal(attribute map[string]events.DynamoDBAttributeValue, out interface{}) error {
av := ddbconversions.AttributeValueMapFrom(attribute)
return attributevalue.UnmarshalMap(av, out)
}

Unmarshal hcl to struct using viper

Trying to Unmarshal a hcl config file to a struct, using viper, this error is returned: 1 error(s) decoding:\n\n* 'NATS' expected a map, got 'slice'. What is missing?
The code:
func lab() {
var c conf
// config file
viper.SetConfigName("draft")
viper.AddConfigPath(".")
viper.SetConfigType("hcl")
if err := viper.ReadInConfig(); err != nil {
log.Error(err)
return
}
log.Info(viper.Get("NATS")) // gives [map[port:10041 username:cl1 password:__Psw__4433__ http_port:10044]]
if err := viper.Unmarshal(&c); err != nil {
log.Error(err)
return
}
log.Infow("got conf", "conf", c)
}
type conf struct {
NATS struct {
HTTPPort int
Port int
Username string
Password string
}
}
And the config file (draft.hcl inside current directory):
NATS {
HTTPPort = 10044
Port = 10041
Username = "cl1"
Password = "__Psw__4433__"
}
Edit
Have checked this struct with hcl package and it gets marshaled/unmarshalled correctly. Also this works correctly with yaml and viper.
There is a difference between these two where log.Info(viper.Get("NATS")) is called. While the hcl version returns a slice of maps, the yaml version returns a map: map[password:__psw__4433__ httpport:10044 port:10041 username:cl1].
Your conf struct is not matching the HCL. When converted to json the HCL looks like below
{
"NATS": [
{
"HTTPPort": 10044,
"Password": "__Psw__4433__",
"Port": 10041,
"Username": "cl1"
}
]
}
So the Conf Struct should look like this
type Conf struct {
NATS []struct{
HTTPPort int
Port int
Username string
Password string
}
}
Modified code
package main
import (
"log"
"github.com/spf13/viper"
"fmt"
)
type Conf struct {
NATS []struct{
HTTPPort int
Port int
Username string
Password string
}
}
func main() {
var c Conf
// config file
viper.SetConfigName("draft")
viper.AddConfigPath(".")
viper.SetConfigType("hcl")
if err := viper.ReadInConfig(); err != nil {
log.Fatal(err)
}
fmt.Println(viper.Get("NATS")) // gives [map[port:10041 username:cl1 password:__Psw__4433__ http_port:10044]]
if err := viper.Unmarshal(&c); err != nil {
log.Fatal(err)
}
fmt.Println(c.NATS[0].Username)
}
I know this question is more than two years old now, but I came across the same issue recently.
I'm using viper to be able to load different configuration files into a Go struct, allowing configuration in JSON, YAML, TOML, HCL, just pick your favourite :)
HCL file format does wrap a map into a slice because it allows redefining a section like:
section = {
key1 = "value"
}
section = {
key2 = "value"
}
which is something that is not supported by the other formats.
And here's how I fixed it:
My solution implies each new block will override any previous definition of the same key, and keep all the others. You can do some merging magic but I didn't need to.
You need to make a hook to convert a slice of maps into a map:
// sliceOfMapsToMapHookFunc merges a slice of maps to a map
func sliceOfMapsToMapHookFunc() mapstructure.DecodeHookFunc {
return func(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) {
if from.Kind() == reflect.Slice && from.Elem().Kind() == reflect.Map && (to.Kind() == reflect.Struct || to.Kind() == reflect.Map) {
source, ok := data.([]map[string]interface{})
if !ok {
return data, nil
}
if len(source) == 0 {
return data, nil
}
if len(source) == 1 {
return source[0], nil
}
// flatten the slice into one map
convert := make(map[string]interface{})
for _, mapItem := range source {
for key, value := range mapItem {
convert[key] = value
}
}
return convert, nil
}
return data, nil
}
}
then you need to create a DecodeHook:
configOption := viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
sliceOfMapsToMapHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
))
the two other hooks are the default ones so you might want to keep them
then you pass the option to the Unmarshal method
viper.Unmarshal(&c, configOption)
With this method you don't need a slice around your structs or your maps. Also that makes it compatible with the other configuration file formats

Resources