As a follow up to my previous post about how to use terrafrom’s built-in looping mechanisms, I set out to write my own custom “data” provider, in this case a new template renderer that would allow me to use Golang’s text/template instead of terraform’s own.
TL;DR — the code is here..
alex-leonhardt/terraform-provider-gotemplate
I started by trying to follow Hashicorp’s Writing Custom Providers documentation, however that does not seem to explain how to write a data provider , and although I had a basic Hello World working, I didn’t get too close to what I actually wanted to do.
So, since there is an existing template provider, I chose to use that as a kind of starting point and hack my way around all the bits I didn’t want/need. Which probably means I didn’t follow “best practice”. What I wanted was simple, I pass through a json encoded variable to the go-template provider and it will read a template file, render it (or fail), and I should be able to access the result, using the .rendered method, just like the original template provider does.
To use it, it’d roughly look like this …
variable "data" {
default = {
"msg" = "Hello World"
"msg2" = [1, 2, 3, 4]
}
}
data "gotemplate" "gotmpl" {
template = "${path.module}/file.tmpl"
data = "${jsonencode(var.data)}"
}
output "tmpl" {
value = "${data.gotemplate.gotmpl.rendered}"
}
As you can see, we have a map variable "data" with a mix of values of string type keys, other types are not allowed it seems — see https://stackoverflow.com/a/8758771
I decided, 2 variables should be sufficient, the path to the template file (template) and the json encoded data (data) itself. I probably won’t go into details of how a provider should be structured as TF’s template provider seems a bit different from the “writing custom providers” instructions — probably as it’s a bit older and possibly one of the first. So, since I followed that example (more or less), my new provider also doesn’t follow it, at least not closely.
To use my new provider, all that’s needed is to compile it into a file called terraform-provider-gotemplate and I can use the gotemplate provider, this is specified in the main.go file here
// Provider is a TF provider
func Provider() *schema.Provider {
return &schema.Provider{
DataSourcesMap: map[string]*schema.Resource{
"gotemplate": dataSourceFile(),
},
}
}
the important part really is in dataSourceFile() which specifies what variables can be accessed using the provider, in my case that was
- template (the path to the go template file)
- data (the json encoded data)
- rendered (the finished render)
other than setting the schema, I also had to make it actually do something, this is done with
func dataSourceFile() *schema.Resource {
return &schema.Resource{
Read: dataSourceFileRead,
which passes dataSourceFileRead function to the schema.Resource‘s Read field (or property? I like to think of it as a property) which must match this function signature
type ReadFunc func(*ResourceData, interface{}) error
which is done with
func dataSourceFileRead(d *schema.ResourceData, meta interface{}) error {
rendered, err := renderFile(d)
if err != nil {
return err
}
d.Set("rendered", rendered)
d.SetId(hash(rendered))
return nil
}
d.Set is important to set the rendered variable with the finished rendered template, so it can be used with output or other providers as a input.
Before setting rendered we retrieved the rendered output by calling the renderFile(d) function, which looks like this
func renderFile(d *schema.ResourceData) (string, error) { | |
var err error | |
tf := template.FuncMap{ | |
"isInt": func(i interface{}) bool { | |
v := reflect.ValueOf(i) | |
switch v.Kind() { | |
case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: | |
return true | |
default: | |
return false | |
} | |
}, | |
"isString": func(i interface{}) bool { | |
v := reflect.ValueOf(i) | |
switch v.Kind() { | |
case reflect.String: | |
return true | |
default: | |
return false | |
} | |
}, | |
"isSlice": func(i interface{}) bool { | |
v := reflect.ValueOf(i) | |
switch v.Kind() { | |
case reflect.Slice: | |
return true | |
default: | |
return false | |
} | |
}, | |
"isArray": func(i interface{}) bool { | |
v := reflect.ValueOf(i) | |
switch v.Kind() { | |
case reflect.Array: | |
return true | |
default: | |
return false | |
} | |
}, | |
"isMap": func(i interface{}) bool { | |
v := reflect.ValueOf(i) | |
switch v.Kind() { | |
case reflect.Map: | |
return true | |
default: | |
return false | |
} | |
}, | |
} | |
var data string // data from tf | |
data = d.Get("data").(string) | |
// unmarshal json from data into m | |
var m = make(map[string]interface{}) // unmarshal data into m | |
if err = json.Unmarshal([]byte(data), &m); err != nil { | |
panic(err) | |
} | |
templateFile := d.Get("template").(string) | |
t, err := template.ParseFiles(templateFile) | |
if err != nil { | |
panic(err) | |
} | |
var contents bytes.Buffer // io.writer for template.Execute | |
tt := t.Funcs(tf) | |
if tt != nil { | |
err = tt.Execute(&contents, m) | |
if err != nil { | |
return "", templateRenderError(fmt.Errorf("failed to render %v", err)) | |
} | |
} else { | |
return "", templateRenderError(fmt.Errorf("error: %v", err)) | |
} | |
return contents.String(), nil | |
} |
The final result is available on Github, use it, make PRs if you like, I have learned quite a bit doing this, although still a bit confused and probably not following the standard way of writing providers, but it was fun getting this to work.
alex-leonhardt/terraform-provider-gotemplate
References:
- https://www.terraform.io/docs/extend/writing-custom-providers.html
- https://github.com/terraform-providers/terraform-provider-template
- https://stackoverflow.com/a/8758771
- https://www.calhoun.io/intro-to-templates-p3-functions/
- https://medium.com/ovni/terraform-templating-and-loops-9a88c0786c5c
Top comments (0)