How to extract data as a List from Set with dynamic keys in terraform - terraform

I have below set in terraform (which is extracted from data module)
{
key1 {
id = "5"
name = "A"
},
key2 {
id = "6"
name = "A"
}
}
keys are dynamic, and it will be n number of them, any value.
How can I get the below result? Please notice it's List of strings
[
"5",
"6"
]
I tried below, but it says Unsupported attribute
output "email_channels_keys" {
value = var.emails.*.id
}

We can use a for lambda expression to iterate on the map(object)) and extract the value for the id key in the object within each map:
output "email_channels_keys" {
value = [ for key, value in data.data_name.block_name.attribute : value.id ]
}
We use a list constructor to instantiate the type as a list. We then iterate over the map and store the string key in the temporary lambda scope variable key, and the object value in the temporary lambda scope variable value. We then access the value of the id key within the object with the normal usage of .id (["id"] is also valid syntax, but conventionally map values are accessed with ["<key>"] and object values with .<key> syntax). The returned value is assigned to email_channels_keys in your outputs.
Note that for your specific use case, you will need to update the data namespace for your specific data that you reference at the beginning of the question, and you may want to update key and value variables for more specific names.

Related

How to combine and sort key-value pair in Terraform

since the last update of the Logicmonitor provider in Terraform we're struggling with a sorting isse.
In LogicMonitor the properties of a device are a name-value pair, and they are presented alfabetically by name. Also in API requests the result is alphabetical. So far nothing fancy.
But... We build our Cloud devices using a module. Calling the module we provide some LogicMonitor properties specially for this device, and a lot more are provided in the module itself.
In the module this looks like this:
`
custom_properties = concat([
{
name = "host_fqdn"
value = "${var.name}.${var.dns_domain}"
},
{
name = "ocid"
value = oci_core_instance.server.id
},
{
name = "private_ip"
value = oci_core_instance.server.private_ip
},
{
name = "snmp.version"
value = "v2c"
}
],
var.logicmonitor_properties)
`
The first 4 properties are from the module and combined with anyting what is in var.logicmonitor_properties. On the creation of the device in LogicMonitor all properties are set in the order the are and no problem.
The issue arises when there is any update on a terraform file in this environment. Due to the fact the properties are presented in alphabetical order, Terraform is showing a lot of changes if finds (but which are in fact just a mixed due to sorting).
The big question is: How can I sort the complete list of properties bases on the "name".
Tried to work with maps, sort and several other functions and examples, but got nothing working on key-value pairs. Merging single key's works fine in a map, but how to deal with name/value pairs/
I think you were on the right track with maps and sorting. Terraform maps do not preserve any explicit ordering themselves, and so whenever Terraform needs to iterate over the elements of a map in some explicit sequence it always do so by sorting the keys lexically (by Unicode codepoints) first.
Therefore one answer is to project this into a map and then project it back into a list of objects again. The projection back into list of objects will implicitly sort the map elements by their keys, which I think will get the effect you wanted.
variable "logicmonitor_properties" {
type = list(object({
name = string
value = string
}))
}
locals {
base_properties = tomap({
host_fqdn = "${var.name}.${var.dns_domain}"
ocid = oci_core_instance.server.id
private_ip = oci_core_instance.server.private_ip
"snmp.version" = "v2c"
})
extra_properties = tomap({
for prop in var.logicmonitor_properties : prop.name => prop.value
})
final_properties = merge(local.base_properties, local.extra_properties)
# This final step will implicitly sort the final_properties
# map elements by their keys.
final_properties_list = tolist([
for k, v in local.final_properties : {
name = k
value = v
}
])
}
With all of the above, local.final_properties_list should be similar to the custom_properties structure you showed in your question except that the elements of the list will be sorted by their names.
This solution assumes that the property names will be unique across both base_properties and extra_properties. If there are any colliding keys between both of those maps then the merge function will prefer the value from extra_properties, overriding the element of the same key from base_properties.
First, use the sort() function to sort the keys in alphabetical order:
sorted_keys = sort(keys(var.my_map))
Next, use the map() function to create a new map with the sorted keys and corresponding values:
sorted_map = map(sorted_keys, key => var.my_map[key])
Finally, you can use the jsonencode() function to print the sorted map in JSON format:
jsonencode(sorted_map)```

Retrieving elements from a list of objects based on a criteria

I have data source that returns a list of objects containing id, name, type.
data " data_source" "some_source" {
filter = ["env:a"]
...
}
I have a another resource that requires a set of ids
resource "another_rerouce" "bar" {
...
set_of_ids = [for i in data.data_source.some_source.objects : i.id]
...
}
Now what I require is only take the ids of the objects which has for e.g. type as live or pending etc. Is there a way i can incorporate this requirement inside [for i in data.data_source.some_source.objects : i.id]?
I am using Terraform v1.2.3.
I would say the answer would be different depending on the number of values you want to check for, i.e., if it is only live and pending, you could use the suggested solution from the comments (h/t: Matt Schuchard):
set_of_ids = [ for i in data.data_source.some_source.objects :
i.id if (i.type == "live" || i.type == "pending")
]
Alternatively, if there will be more than two values, you could create a local variable of type list(string) and assign all the values that are acceptable:
locals {
acceptable_types = ["live", "pending"]
}
Then, in the for loop, you could do the following:
set_of_ids = [ for i in data.data_source.some_source.objects :
i.id if contains(local.acceptable_types, i.type)
]
Here you would use the contains built-in function [1] to check if the type is inside of the acceptable/allowed types list.
[1] https://www.terraform.io/language/functions/contains

Preserve list ordering when creating maps in Terraform 0.12?

I have the following snippets in my configuration - the idea is to change current logic/syntax from 0.11 to 0.12. First, I am creating a map from lists,
my_vars = zipmap(
var.foo_vars,
flatten(data.terraform_remote_state.foo.*.outputs.some_id)
)
Then iterate over it to produce some key value pairs.
...
"var": [for key in keys(local.my_vars) :
{
name = key
value = lookup(local.my_vars, key)
}
],
...
And here is the relevant tfvars configuration.
foo_vars = [
"A",
"B",
"C"
]
The problem is that this logic doesn't seem to preserver order and I can't figure out a good way to make this happen. From what I understand, once you turn the lists into a map with zipmap, the order is recalculated. Is there anything that can be done to have the original order preserved?
I'm not tied to the current solution, so maybe there is a way to generate the key/values that doesn't require a map to be created first and can be done instead with only the two lists?
~ foo = [
{
name = "A"
value = "1"
},
- {
- name = "B"
- value = "2"
},
{
name = "C"
value = "3"
},
+ {
+ name = "B"
+ valueFrom = "2"
},
]
The important thing here is that, as you've noticed, Terraform's map type is an unordered map which identifies elements only by their keys, not by permission. Therefore if you have a situation where you need to preserve the order of a sequence then a map is not a suitable data structure to use.
I have a suspicion that keeping things ordered may not actually be necessary to solve your underlying problem here, but I can't tell from the information you've shared what the real-world meaning of all of these values is, so I'm going to answer on the assumption that you do need to preserve the order. If you are working with ordered sequences only because you are creating multiple instances of a resource using count, I'd suggest that you consider using resource for_each instead, which may allow you to solve your underlying problem in a way that is not sensitive to the order of items in var.foo_vars.
Given two lists of the same length, you can produce a new list that combines the corresponding elements from each list by writing a for expression like this:
locals {
my_vars = [
for i, some_id in data.terraform_remote_state.foo.*.outputs.some_id : {
name = var.foo_vars[i]
value = some_id
}
]
}
The above relies on the fact that i index values from one list are correlated with the element of the same index in the other list, and so we can use the i from the data source instances to access the corresponding element of var.foo_vars.

How to pass list of s3 arns inside the terraform data resource aws_iam_policy_document

I am trying to pass multiple values to pricipals's identifiers in the data resource "aws_iam_policy_document". getting the following error
Inappropriate value for attribute "identifiers": element 0: string required.
s3_values variable is defined type = any and set the values as
....
s3_values:
bucket: bucketname1
s3_arns:
- arn:aws:iam::1234567890:root
- arn:aws:iam::2345678901:role/s3-read-role
data "aws_iam_policy_document" "s3_policy" {
count = length(var.s3_arns)
statement {
sid = "1"
effect = "Allow"
principals {
type = "AWS"
identifiers = ["${var.s3_values[count.index]["s3_arns"]}"]
}
actions = ["s3:PutObject"]
resources = ["arn:aws:s3:::${var.s3_values[count.index]["bucket"]}/*"]
}
}
I get the following error
Inappropriate value for attribute "identifiers": element 0: string required.
its working , when only one value is passed , but not working when we pass multiple values to the variable s3_arns.
It looks like you're trying to create multiple policy documents for a single S3 bucket. Rather than using count to create many documents, it would be best if you created a single policy document that gives access to each ARN you pass.
Currently it works for one ARN because the identifiers field gets passed a single string and creates a list with one string element. When you pass a list of ARNs, the identifiers field is instead creating a list with a list element that contains the ARN strings.
I would fix this by making the s3_arns field always be a list of strings, and removing the count field on the data resource. Once you do that you can change the line identifiers to be identifiers = var.s3_values.s3_arns and the resources line to be resources = ["arn:aws:s3:::${var.s3_values.bucket}/*"]

Terraform - How to restrict an input variable to a list of possible choices

I have a variable that the user will input during run time. Lets say the variable name is region. However, I want the execution to be only successful if the user picks a value from one of the values defined in a list/ choices.
how can I restrict it so the user's selection has to match values that are considered acceptable in the variable definition?
Stumbled across this question.
Since v0.13.0 input validation has been possible directly via the input variables. Thus you can actually achieve this with a snippet such as below.
variable "test_variable" {
type = string
description = "some test value"
validation {
condition = contains(["item1", "item2", "item3"], var.test_variable)
error_message = "Valid values for var: test_variable are (item1, item2, item3)."
}
}
Read more here - https://www.hashicorp.com/blog/custom-variable-validation-in-terraform-0-13
One solution:
variable "values_list" {
description = "acceptable values"
type = "list"
default = ["true", "false"]
}
variable "somevar" {
description = "must be true or false"
}
resource "null_resource" "is_variable_value_valid" {
count = "${contains(var.values_list, var.somevar) == true ? 0 : 1}"
"ERROR: The somevar value can only be: true or false" = true
}
If you pass a value different than "true" or "false" for the "somevar" variable, Terraform will throw an error and stop. The disadvantage is that you have to list all values in the default block of values_list.
Source: https://stackoverflow.com/a/54256780/1364793
Terraform currently has no first-class feature for this, but you can achieve the desired effect (albeit with a less-helpful error message) by asking Terraform to look the value up in a map:
variable "example" {
description = "must be a, b, or c"
}
locals {
allowed_example_values = { for v in ["a", "b", "c"] : v => v }
checked_example = local.allowed_example_values[var.example] # will fail if var.example is invalid
}
Because the values in the allowed_example_values map are the same as the keys, you can then use local.checked_example in place of var.example everywhere else in the module to set up the dependency graph such that it's impossible for an invalid value to be used.
Some caveats/limitations:
You can't customize the error message that Terraform will return when the value is invalid. Instead, Terraform will return the generic error about the key not matching any element in the map. However, it will include the source code snippet from the affected line in the error message (in Terraform 0.12 or later) so the comment at the end of that line should be visible in the error message and thus provide an additional clue to the user as to what might be wrong.
This works fully only for string values, because map keys are always strings in Terraform. Using other primitive types can work as a result of Terraform's automatic conversions from bool and number to string, but you should be sure to explicitly declare the type of the variable (using e.g. type = number) to ensure that Terraform will normalize incoming values into the expected type before looking them up in the map. This technique will not work at all for collection and structural types.
I wouldn't suggest using strings "true" and "false" since Terraform has a bool type which can represent that more intuitively. If you are using Terraform 0.11 and earlier then it's not possible to constrain to bool, but in Terraform 0.12 and later you can declare a variable as type = bool and then Terraform will handle the validation automatically.

Resources