Golang Templating: Substituting Values Between Different YAML Files

Written by tiago-melo | Published 2023/05/23
Tech Story Tags: golang | templating | yaml | programming | programming-languages | template | web-app-development | programming-tips

TLDR[Go] templating is a powerful feature that allows you to generate text output by replacing placeholders (variables) in a template with their corresponding values. It's a convenient way to generate dynamic content, including [YAML] files. In this tutorial we'll see how we can use [Go templation] to replace values in a [YamL] file with values from another [Helm](https://helm.sh/) file.via the TL;DR App

Go templating is a powerful feature provided by the language that allows you to generate text output by replacing placeholders (variables) in a template with their corresponding values. It's a convenient way to generate dynamic content, including YAML files.

Helm, a package manager for Kubernetes, utilizes Go templating to generate Kubernetes manifest files from templates.

I needed the same in a project of mine.

In this tutorial, we'll see how we can use Go templating to replace values in a YAML file with values from another YAML file, similar to what Helm does.

The reference Github repo can be found here.

Template file and values

Suppose we want to replace our template with some values like Helm does.

template/template.yaml

apiVersion: v1
kind: Deployment
metadata:
  name: {{ .AppName }}
spec:
  replicas: {{ .ReplicaCount }}
  template:
    spec:
      containers:
      - name: {{ .AppName }}
        image: {{ .Image }}

template/values.yaml

AppName: my-app
ReplicaCount: 3
Image: myregistry/my-app:v1.0.0

Template parsing

parser/parser.go

// Copyright (c) 2023 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package parser


import (
    "html/template"
    "io"
    "os"


    "github.com/pkg/errors"
    "gopkg.in/yaml.v2"
)


// For ease of unit testing.
var (
    parseFile           = template.ParseFiles
    openFile            = os.Open
    createFile          = os.Create
    ioReadAll           = io.ReadAll
    yamlUnmarshal       = yaml.Unmarshal
    executeTemplateFile = func(templateFile *template.Template, wr io.Writer, data any) error {
        return templateFile.Execute(wr, data)
    }
)


// valuesFromYamlFile extracts values from yaml file.
func valuesFromYamlFile(dataFile string) (map[string]interface{}, error) {
    data, err := openFile(dataFile)
    if err != nil {
        return nil, errors.Wrap(err, "opening data file")
    }
    defer data.Close()
    s, err := ioReadAll(data)
    if err != nil {
        return nil, errors.Wrap(err, "reading data file")
    }
    var values map[string]interface{}
    err = yamlUnmarshal(s, &values)
    if err != nil {
        return nil, errors.Wrap(err, "unmarshalling yaml file")
    }
    return values, nil
}


// Parse replaces values present in the template file
// with values defined in the data file, saving the result
// as an output file.
func Parse(templateFile, dataFile, outputFile string) error {
    tmpl, err := parseFile(templateFile)
    if err != nil {
        return errors.Wrap(err, "parsing template file")
    }
    values, err := valuesFromYamlFile(dataFile)
    if err != nil {
        return err
    }
    output, err := createFile(outputFile)
    if err != nil {
        return errors.Wrap(err, "creating output file")
    }
    defer output.Close()
    err = executeTemplateFile(tmpl, output, values)
    if err != nil {
        return errors.Wrap(err, "executing template file")
    }
    return nil
}

  1. First, we call 'template.ParseFiles' to create a new template and parse the template definitions from the named files.

  2. Then, we need to read the YAML file and parse it - our 'valuesFromYamlFile' returns a 'map[string]interface{}' containing keys and values found. We're using gopkg.in/yaml.v2 for that.

  3. Next, we create the output file in which the output will be written - that is, the template file 'template/template.yaml' with all placeholders replaced by the values we defined in 'template/values.yaml'. If you do not want to save it to a new file, you can do just 'templateFile.Execute(os.Stdout, values)' and then the output will be printed to the console.

  4. Finally, we execute the template to replace all placeholders in the template file and write the output to a new file.

Using it

cmd/main.go

// Copyright (c) 2023 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package main


import (
    "fmt"
    "os"


    "tiago.com/parser"
)


func run() error {
    const templateFile = "template/template.yaml"
    const dataFile = "template/values.yaml"
    const outputFile = "parsed/parsed.yaml"
    if err := parser.Parse(templateFile, dataFile, outputFile); err != nil {
        return err
    }
    fmt.Printf("file %s was generated.\n", outputFile)
    return nil
}


func main() {
    if err := run(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

Running it

$ make run

file parsed/parsed.yaml was generated.

Output

parsed/parsed.yaml

apiVersion: v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: my-app
        image: myregistry/my-app:v1.0.0

And that’s it!


Written by tiago-melo | Senior Software Engineer
Published by HackerNoon on 2023/05/23