How to use format for list of strings in terraform 0.12.20? - terraform

I'm creating data resource to create a policy document for allowing users to access rds, but i'm stuck on how to use format to pass account_id and rds's resource_id,
Code:
data "aws_iam_policy_document" "iam_authentication_doc" {
depends_on = [aws_db_instance.name]
statement {
effect = "Allow"
actions = [
"rds-db:connect"
]
resources = flatten([format("arn:aws:rds-db:us-east-1:${var.account_id}:dbuser:${aws_db_instance.name.resource_id}/%s", var.usernames)])
}
}
Error:
resources = flatten([format("arn:aws:rds-db:us-east-1:${var.account_id}:dbuser:${aws_db_instance.pgauth.resource_id}/%s", var.usernames)])
|----------------
| aws_db_instance.pgauth.resource_id is "db-xxxxxxxxxxxxxxxx"
| var.account_id is 8.12345678901+11
| var.usernames is list of string with 12 elements
Call to function "format" failed: unsupported value for "%s" at 75: string
required.
I tried passing
[formatlist("arn:aws:rds-db:us-east-1:%s:dbuser:%s/%s", var.account_id, aws_db_instance.pgauth.resource_id, var.amp_usernames)]
got an error
22: resources = [formatlist("arn:aws:rds-db:us-east-1:%s:dbuser:%s/%s", var.account_id, aws_db_instance.name.resource_id, var.usernames)]
|----------------
| aws_db_instance.name.resource_id is "db-xxxxxxxxxxxxxxx"
| var.account_id is "123456789012"
| var.usernames is list of string with 12 elements
Inappropriate value for attribute "resources": element 0: string required.
I want resources like
arn:aws:rds-db:us-east1:1234567890:dbuser:db-xxxxxxxxxxxxxx/foo,
arn:aws:rds-db:us-east1:1234567890:dbuser:db-xxxxxxxxxxxxxx/bar,
arn:aws:rds-db:us-east1:1234567890:dbuser:db-xxxxxxxxxxxxxx/tim

The first example with format did not work because format expects all of its arguments to be single values and it produces a single value.
As you've seen, the formatlist function is one way to solve your problem: it produces a list as its result, and if any of its arguments are lists then it repeats the formatting process once for each set of elements with the same index across the lists.
Your second example didn't work because you wrapped the call to formatlist in [ ... ], which constructs a list. Becuse formatlist returns a list itself, the result was therefore a list of lists of strings rather than just a list of strings.
We can get it working by removing the redundant brackets:
resources = formatlist("arn:aws:rds-db:us-east-1:%s:dbuser:%s/%s", var.account_id, aws_db_instance.name.resource_id, var.usernames)
Another way to write this is using a for expression, which will perhap make the repetition more explicit in your configuration:
resources = [for u in var.usernames : "arn:aws:rds-db:us-east-1:${var.account_id}:dbuser:${aws_db_instance.name.resource_id}/${u}"]
Which one is easier to understand is of course subjective: the formatlist approach shows the format string up front but it leaves it implied that we're repeating based on elements of var.usernames. The for expression approach pushes the template to the end of the line, but it makes the repetition based on var.usernames more explicit.

resources = flatten(formatlist("arn:aws:rds-db:us-east-1:%s:dbuser:%s/%s", var.account_id, aws_db_instance.pgauth.resource_id, var.usernames))
i did not specify the type for account_id.

Related

retrieve values from a list based on criterea using terraform syntax HCL

I am looking for a terraform expression to retrieve values from a list, i have a list of values
namespaces = [blue,red,green,ns-blue,ns-green,ns-grey]
I would like to retrieve in list format just the values contains "ns", as a result i must get:
namepsace-filtred = [ns-blue,ns-green,ns-grey]
thanks in advance.
Assuming you have a list of strings for a variable namespace:
variable "namespaces" {
default = ["blue", "red", "green", "ns-blue", "ns-green", "ns-grey"]
}
You can use a for with the regex function to check if a string contains a substring. Also, you have to use the can function to transform the result of the regex to a boolean:
locals {
namepsace_filtred = [for ns in var.namespaces : ns if can(regex("ns", ns))]
}
The result of this should be something like this:
namepsace_filtred = [
"ns-blue",
"ns-green",
"ns-grey",
]

Remove a map item in terraform

If I use the following yamldecode, I get the following output:
output "product" {
value = distinct(yamldecode(file("resource/lab/abc.yaml"))["account_list"]["resource_tags"][*]["TAG:product"])
}
Output:
+ product = [
+ "fargate",
+ "CRM",
]
I want fargate to be removed from my output and the expected output is this:
+ product = [
+ "CRM"
]
Please let me know how I can do this.
output "product" {
value = compact([for x in distinct(yamldecode(file("resource/lab/abc.yaml"))["account_list"]["resource_tags"][*]["TAG:product"]) : x == "fargate" ? "" : x])
}
I get this output:
test = [
"enter",
]
compact function solved the problem.
The Terraform language is based on functional principles rather than imperative principles and so it does not support directly modifying an existing data structure. Instead, the best way to think about goals like this is how to define a new value which differs from your existing value in some particular way.
For collection and structural types, the most common way to derive a new value with different elements is to use a for expression, which describes a rule for creating a new collection based on the elements of an existing collection. For each element in the source collection we can decide whether to use it in the result at all, and then if we do decide to use it we can also describe how to calculate a new element value based on the input element value.
In your case you seem to want to create a new list with fewer elements than an existing list. (Your question title mentions maps, but the question text shows a tuple which is behaving as a list.)
To produce a new collection with fewer elements we use the optional if clause of a for expression. For example:
[for x in var.example : x if x != "fargate"]
In the above expression, x refers to each element of var.example in turn. Terraform first evaluates the if clause, and expects it to return true if the element should be used. If so, it will then evaluate the expression immediately after the colon, which in this case is just x and so the result element is identical to the input element.
Your example expression also includes some unrelated work to decode a YAML string and then traverse to the list inside it. That expression replaces var.example in the above expression, as follows:
output "products" {
value = toset([
for x in yamldecode(file("${path.module}/resource/lab/abc.yaml"))["account_list"]["resource_tags"][*]["TAG:product"]: x
if x != "fargate"
])
}
I made some other small changes to the above compared to your example:
I named the output value "products" instead of "product", because it's returning a collection and it's Terraform language idiom to use plural names for collection values.
I wrapped the expression in toset instead of distinct, because from your description it seems like this is an unordered collection of unique strings rather than an ordered sequence of strings.
I added path.module to the front of the file path so that this will look for the file in the current module directory. If your output value is currently in a root module then this doesn't really matter because the module directory will always be the current working directory, but it's good practice to include this so that it's clear that the file belongs to the module.
Therefore returning a set is more appropriate than returning a list because it communicates to the caller of the module that they may not rely on the order of these items, and therefore allows the order to potentially change in future without it being a breaking change to your module.

Terraform split/join/list

I recently found the following line of terraform in our code (some values sanitized):
subnet_ids = [ "${split(",", var.xxx_lb ? join(",", data.yyy_ids.private.ids) : join(",", concat(data.yyy_ids.public.ids, list(""))))}" ]
I'm trying to understand why code would be written this way. More specifically, what is the final join doing? Pulling it out for clarity:
join(",", concat(data.yyy_ids.public.ids, list("")))
It seems that someone (no longer at the company) was trying to ensure that a non-empty list is returned. We definitely don't want the empty ("") item in the list.
So, the questions here are:
What logically is going on in this statement?
Is there a better way?
If there is not a better way, how can we remove the empty entry from
the resulting list?
Update for others who may run into this sort of code:
Terraform versions lower than 0.12 conditionals don't work with lists, so join/split is used to turn lists into strings and then back to lists:
https://github.com/hashicorp/terraform/issues/12453
What logically is going on in this statement?
The original author attempts to create a list with the subnet ids
a) The first split statement will take a string in this case the subnet ids and return them as a list, by splitting them based on the delimiter ,
var.xxx_lb ? clause_if_true : clause_if_false
b) Next terraform will evaluate this variable as a boolean and according to the result you will get the public or the private subnet ids, by employing the ternary operator syntax
join(",", data.yyy_ids.private.ids)
c) In case the boolean value is true, terraform will examine this part
This will return a string by joining the items of the list.
And add the delimeter ,. I assume the reason that he attempts to join them as a string is to be accordance with the section a)
join(",", concat(data.yyy_ids.public.ids, list("")))
d) If the boolean value in b) evaluates to false terraform will examine this part.
The concat function takes as input lists and returns them as a single list.
And then performs the same logic as in c)
The list function is deprecated, tolist should be used instead.
Is there a better way?
I would employ a straight forward way. Check the boolean value, if it is true get the list with private ids. If false the public ones.
subnet_ids = var.xxx_lb ? data.yyy_ids.private.ids : data.yyy_ids.public.ids

How can I convert a list to a string in Terraform?

join works BUT i want to keep the double quotes join gives me this
[ben,linda,john]
BUT i want this
["ben", "linda", "john"]
this is getting crazy, spent over 2 hours trying to fix this
i want to pass in a list as a string variable
why can't terraform just take in my list as a string? why is this so difficult?
so i have
name = ["ben", "linda", "john"]
and i want to pass this to variable used in terrform
var.name
why can't terrform take this as is?
i get the error saying epxtected a string and i can not find a solution online after sarching everywhere
i have been able to get
[ ben,linda,john ] using join(",", var.name) but i want ["ben", "linda", "john"]
$ terraform --version
Terraform v0.12.18
+ provider.aws v2.42.0
+ provider.template v2.1.2
Conversion from list to string always requires an explicit decision about how the result will be formatted: which character (if any) will delimit the individual items, which delimiters (if any) will mark each item, which markers will be included at the start and end (if any) to explicitly mark the result as a list.
The syntax example you showed looks like JSON. If that is your goal then the easiest answer is to use jsonencode to convert the list directly to JSON syntax:
jsonencode(var.names)
This function produces compact JSON, so the result would be the following:
["ben","linda","john"]
Terraform provides a ready-to-use function for JSON because its a common need. If you need more control over the above decisions then you'd need to use more complex techniques to describe to Terraform what you need. For example, to produce a string where each input string is in quotes, the items are separated by commas, and the entire result is delimited by [ and ] markers, there are three steps:
Transform the list to add the quotes: [for s in var.names : format("%q", s)]
Join that result using , as the delimiter: join(", ", [for s in var.names : format("%q", s)])
Add the leading and trailing markers: "[ ${join(",", [for s in var.names : format("%q", s)])} ]"
The above makes the same decisions as the JSON encoding of a list, so there's no real reason to do exactly what I've shown above, but I'm showing the individual steps here as an example so that those who want to produce a different list serialization have a starting point to work from.
For example, if the spaces after the commas were important then you could adjust the first argument to join in the above to include a space:
"[ ${join(", ", [for s in var.names : format("%q", s)])} ]"

How to check if string contains a substring in terraform interpolation?

How do you check if a terraform string contains another string?
For example, I want to treat terraform workspaces with "tmp" in the name specially (e.g. allowing rds instances to be deleted without a snapshot), so something like this:
locals
{
is_tmp = "${"tmp" in terraform.workspace}"
}
As far as I can tell, the substr interpolation function doesn't accomplish this.
For terraform 0.12.xx apparently you are suppose to use regexall to do this.
From the manual for terraform 0.12.XX:
regexall() documentation
regexall can also be used to test whether a particular string matches a given pattern, by testing whether the length of the resulting list of matches is greater than zero.
Example from the manual:
> length(regexall("[a-z]+", "1234abcd5678efgh9"))
2
> length(regexall("[a-z]+", "123456789")) > 0
false
Example applied to your case in terraform 0.12.xx syntax should be something like:
locals
{
is_tmp = length(regexall(".*tmp.*", terraform.workspace)) > 0
}
It also specifically says in the manual not to use "regex" but instead use regexall.
If the given pattern does not match at all, the regex raises an error. To test whether a given pattern matches a string, use regexall and test that the result has length greater than zero.
As stated above this is because you will actually get an exception error when you try to use it in the later versions of 0.12.xx that are out now when you run plan. This is how I found this out and why I posted the new answer back here.
You can indirectly check for substrings using replace, e.g.
locals
{
is_tmp = "${replace(terraform.workspace, "tmp", "") != terraform.workspace}"
}
Like #MechaStorm, with Terrafor 0.12.7+ you can use regex to return a Boolean value if your string contains a particular substring
locals {
is_tmp = contains(regex("^(?:.*(tmp))?.*$",terraform.workspace),"tmp")
}
The regex query returns a list of capture groups for any characters before tmp, tmp if found, any characters after tmp. Then contains looks for "tmp" in the list and returns true or false. I am using this type of logic in my own terraform.
Length of the list produced by split function is greater than one when separtor is a substring.
locals {
is_tmp = length(split("tmp", terraform.workspace)) > 1
}
Use replace( string, search, replace ) as in the snippet:
// string contains ABBA = result is ABBA
output "match" {
value = "${ replace("xxxABBAyyy", "/(?:.*)(ABBA)(?:.*)/", "$1") }"
}
// string doesn't contain ABBA = result is original string
output "no_match" {
value = "${ replace("xxxBABAyyy", "/(?:.*)(ABBA)(?:.*)/", "$1")}"
}
// string contains ABBA (ingorecase) = result is AbBA
output "equals_ignorecase" {
value = "${ replace("xxxAbBAyyy", "/(?:.*)((?i)ABBA)(?:.*)/", "$1")}"
}
An output of terraform apply is:
Outputs:
equals_ignorecase = AbBA
match = ABBA
no_match = xxxBABAyyy
In terraform 0.12.7, we now have regex . This may help simplify some code and make it readable to some (perhaps?)
> regex("[a-z]+", "53453453.345345aaabbbccc23454")
aaabbbccc
I use this way to check if bucket name start with test-tmp
eg. test-tmp, test-tmp-app1, test-tmp-db1 etc..
is_test_bucket = can(regex("^(test-tmp){1}($|-{1}.*$)", var.bucket_name))
Something that makes sense reading, IMHO:
locals {
is_tmp = can(regex("tmp", terraform.workspace))
}
This works because the regex function will raise an error if no matches are found.
Bonus: since Terraform 1.3.x, there are the new startswith and endswith functions that can be handy in a good amount of cases.

Resources