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.
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
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
}
First, we call 'template.ParseFiles' to create a new template and parse the template definitions from the named files.
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.
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.
Finally, we execute the template to replace all placeholders in the template file and write the output to a new file.
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)
}
}
$ make run
file parsed/parsed.yaml was generated.
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!