Reputation: 59989
Is there a way to detect the type of a variable in Terraform? Say, I have a module input variable of type any
, can I do some kind of switch, depending on the type?
variable "details" {
type = any
}
local {
name = var.details.type == map ? var.details["name"] : var.details
}
What I want to archive is, to be able to pass either a string as shorthand or a complex object with additional keys.
module "foo" {
details = "my-name"
}
or
module "foo" {
details = {
name = "my-name"
age = "40"
}
}
I know this example doesn't make much sense and you would like to suggest to instead use two input vars with defaults. This example is just reduced to the minimal (non)working example. The end goal is to have a list of IAM policy statements, so it is going to be a list of lists of objects.
Upvotes: 8
Views: 17851
Reputation: 21
[EDITED]
I had a use-case where type()
would have been pretty helpful to use in my code for a Terraform module I was creating. But, as mentioned, the type()
function is only usable currently (Terraform v1.5.6) in the Terraform console, which makes it bit more difficult to do type inference, but not impossible!
So here we go (also, I appreciate the feedback! I'm new to stackoverflow):
My module was using Terraform to render YAML for SSM document parameters, placing them onto SSM document runbooks, and inject them into a cloudformation template. I wanted to dynamically infer the types of my parameters but render the type verbiage that AWS syntax expects. But type inference is tricky in terraform.
I should note that the only AWS parameter type I wasn't sure how to infer was "MapList" (described here: https://docs.aws.amazon.com/systems-manager/latest/userguide/documents-syntax-data-elements-parameters.html#top-level).
Here is a full workflow example:
~/terraform/plans/my_plan
. And my terraform module lives here: ~/terraform/modules/render_params
So, starting with the main.tf file in my_plan
, to test all of the types (excluding mixed types), we have something that looks like this:
module "testing_some_renders" {
source = "../../modules/render_params"
parameters = jsonencode({
a = "b"
b = "2"
c = 2
d = "01"
e = 01
f = {
"a" = "b"
}
g = true
h = false
i = ["a", "b"]
)
}
variables.tf
file in the render_params
module directory would look like this:variable parameters {
type = string
default = ""
description = "A jsonencoded string of key-value pairs whose content will be type inferred."
templatefile()
function call would live in render_params/templates/params.yaml
And would look like this:"parameters":
${indent(2, parameters)}
main.tf
would look like this:locals {
type_chart = {
# https://docs.aws.amazon.com/systems-manager/latest/userguide/documents-syntax-data-elements-parameters.html#top-level
"string" = "String"
"list(string)" = "StringList"
"number" = "Integer"
"bool" = "Boolean"
"map(string)" = "StringMap"
}
type_inferred_parameters = yamlencode({
for k, v in jsondecode(var.parameters) : k => {
type = can(tobool(v)) && v != null && can(tonumber(v)) == false ? local.type_chart["bool"] : (
can(tolist(v)) && v != null ? local.type_chart["list(string)"] : (
can(tomap(v)) && v != null ? local.type_chart["map(string)"] : (
can(tonumber(v)) && format("%s", v) != v ? local.type_chart["number"] : (
format("%s", v) == v && can(tobool(v)) == false ? local.type_chart["string"] : (
"Indeterminable"
)
)
)
)
)
description = "Managed By Terraform."
default = v
}
})
rendered_yaml = templatefile("${path.module}/templates/params.yaml", {
parameters = local.type_inferred_parameters
})
}
output "final_render" {
value = local.rendered_yaml
}
"parameters":
"a":
"default": "b"
"description": "Managed By Terraform."
"type": "String"
"b":
"default": "2"
"description": "Managed By Terraform."
"type": "String"
"c":
"default": 2
"description": "Managed By Terraform."
"type": "Integer"
"d":
"default": "01"
"description": "Managed By Terraform."
"type": "String"
"e":
"default": 1
"description": "Managed By Terraform."
"type": "Integer"
"f":
"default":
"a": "b"
"description": "Managed By Terraform."
"type": "StringMap"
"g":
"default": true
"description": "Managed By Terraform."
"type": "Boolean"
"h":
"default": false
"description": "Managed By Terraform."
"type": "Boolean"
"i":
"default":
- "a"
- "b"
"description": "Managed By Terraform."
"type": "StringList"
Upvotes: 2
Reputation: 1762
terraform v1.0+ introduces a new function type() for this purpose. See https://www.terraform.io/language/functions/type
Upvotes: -1
Reputation: 74249
Terraform v0.12.20 introduced a new function try
which can be used to concisely select between different ways of retrieving a value, taking the first one that wouldn't produce an error.
variable "person" {
type = any
# Optional: add a validation rule to catch invalid types,
# though this feature remains experimental in Terraform v0.12.20.
# (Since this is experimental at the time of writing, it might
# see breaking changes before final release.)
validation {
# If var.person.name succeeds then var.person is an object
# which has at least the "name" attribute.
condition = can(var.person.name) || can(tostring(var.person))
error_message = "The \"person\" argument must either be a person object or a string giving a person's name."
}
}
locals {
person = try(
# The value of the first successful expression will be taken.
{name = tostring(var.person)}, # If the value is just a string
var.person, # If the value is not a string (directly an object)
)
}
Elsewhere in the configuration you can then write local.person.name
to obtain the name, regardless of whether the caller passed an object or a string.
The remainder of this answer is an earlier response that now applies only to Terraform versions between v0.12.0 and v0.12.20.
There is no mechanism for switching behavior based on types in Terraform. Generally Terraform favors selecting specific types so that module callers are always consistent and Terraform can fully validate the given values, even if that means a little extra verbosity in simpler cases.
I would recommend just defining details
as an object and having the caller explicitly write out the object with the name
attribute, in order to be more explicit and consistent:
variable "details" {
type = object({
name = string
})
}
module "example" {
source = "./modules/example"
details = { name = "example" }
}
If you need to support two different types, the closest thing in the Terraform language would be to define two variables and detect which one is null
:
variable "details" {
type = object({
name = string
})
default = null
}
variable "name" {
type = string
default = null
}
local {
name = var.name != null ? var.name : var.details.name
}
However since there is not currently a way to express that exactly one of those two must be specified, the module configuration you write must be ready to deal with the possibility that both will be set (in the above example, var.name
takes priority) or that neither will be set (in the above example, the expression would produce an error, but not a very caller-friendly one).
Upvotes: 9