Go templates as the source of a terraform provider
The AWX API describes every resource, so the provider is thirteen templates and a config file that says where the description lies.
The AWX provider isn’t written so much as rendered, there are thirteen templates in the repository and everything else in Go comes out of them.
That sounds clever and it mostly isn’t, text/template is in the standard library, the API hands us a description of every endpoint, and the whole job is turning one into the other.
A resource needs three things generated, the model, the schema, and the bits that talk to the API, so the templates are split along the same lines.
terraform/tf_object.go.tplterraform/tf_object_model.partial.tplterraform/tf_object_resource.partial.tplterraform/tf_object_data_source.partial.tplSo the partials are where the repetition lives.
A schema attribute looks the same whether it belongs to an organization or a job template, so it’s one define block that everything else calls.
{{- define "attrSchema" -}}"{{ $key | lowerCase }}": schema.{{ $value.Generated.AttributeType }}Attribute{ Description: {{ escape_quotes (or $value.Description $value.Label) }},{{- if $value.IsSensitive }} Sensitive: true,{{- end }}{{- if $value.IsRequired }} Required: true,{{- else }} Optional: true,{{- end }}}{{- end }}If you’ve written templates before you’ll recognise the {{- everywhere, and that’s the tax.
Go templates emit whitespace exactly as you left it, so the minus signs are load bearing and getting one wrong gives you Go that doesn’t compile for reasons that have nothing to do with the logic.
We run everything through gofmt afterwards anyway, so the template output only has to be syntactically valid rather than pretty, which takes some of the pressure off.
The template functions are the other half.
lowerCase, escape_quotes and tf_element_type aren’t in the standard library, they’re a FuncMap we pass in, and that’s where anything too fiddly for a template goes.
The rule I settled on is that a template is allowed to decide layout and nothing else, and the moment one starts deciding what something means it goes into a function instead and the template just calls it.
Now the part that took me longest to accept, which is that the API description is right most of the time and wrong in specific boring ways, so there’s a config file per AWX version where we write down exactly how.
Some fields come back as a string that’s really an enum.
Some are write only and must never be read back into state, because AWX returns a placeholder and terraform would see a permanent diff.
Some endpoints don’t support PATCH.
None of that is in the OPTIONS output.
So we carry it ourselves, per version, and the generator applies it on the way through.
That file is the one part of this that nobody can generate, and it’s also the part that makes everything else work.
The newest thing in the generator warns when an override does nothing.
An override exists to correct the description, and when AWX fixes it upstream the override becomes a lie that sits there for two years until somebody goes looking.