DEV Community

Alex Leonhardt
Alex Leonhardt

Posted on • Originally published at Medium on

7

Writing my own terraform template (data) provider

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
}
view raw renderFile.go hosted with ❤ by GitHub

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:


Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Billboard image

Try REST API Generation for Snowflake

DevOps for Private APIs. Automate the building, securing, and documenting of internal/private REST APIs with built-in enterprise security on bare-metal, VMs, or containers.

  • Auto-generated live APIs mapped from Snowflake database schema
  • Interactive Swagger API documentation
  • Scripting engine to customize your API
  • Built-in role-based access control

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay