Create a set from a list of sets - terraform

I am trying to create a predefined set of IAM roles.
locals {
default_iam_roles = {
group1 = {
name = "group:group1-group#mydomain.com"
roles = toset([
"roles/viewer"
])
}
group2 = {
name = "group:group2-group#mydomain.com"
roles = toset([
"roles/owner"
])
}
}
formatted_iam_roles = [ for member in local.default_iam_roles : setproduct([member.name], member.roles) ]
}
If I print local.formatted_iam_roles I get the following:
[
toset([
[
"group:group1-group#mydomain.com",
"roles/viewer",
],
]),
toset([
[
"group:group2-group#mydomain.com",
"roles/owner",
],
]),
]
Now I want to create a single set which contains all of the combinations contained in the list, so that I can feed it to a resource on a for_each statement, but I am not able to find the logic for this.
The expected output would be:
toset([
[
"group:group1-group#mydomain.com",
"roles/viewer",
],
[
"group:group2-group#mydomain.com",
"roles/owner",
]
])

The operation of taking multiple sets and deriving a new set which contains all of the elements across all of the input sets is called union and so Terraform's function for it is called setunion to follow that.
locals {
all_iam_roles = setunion(local.formatted_iam_roles...)
}
The ... symbol after the argument tells Terraform that it should use each element of local.formatted_iam_roles as a separate argument to setunion, because that function is defined as taking an arbitrary number of arguments that are all sets, rather than as taking a list of sets.
It might be helpful to think of setunion as being a similar sort of function as concat. The concat function joins multiple lists together, preserving each list's element order and the order the lists are given. Set elements don't have any particular order and contain each distinct value only once, and so the behavior of setunion is different but its purpose is related.

Your solution in the question was close, you just have to apply a flatten function to the setproduct output.
locals {
# ...
formatted_iam_roles = [for member in local.default_iam_roles : flatten(setproduct([member.name], member.roles))]
}
The output will be the following:
formatted_iam_roles = [
[
"group:group1-group#mydomain.com",
"roles/viewer",
],
[
"group:group2-group#mydomain.com",
"roles/owner",
],
]
Now, if you really want it to make a set for the final result, you can use toset, most like it will be pointless:
formatted_iam_roles = toset([for member in local.default_iam_roles : flatten(setproduct([member.name], member.roles))])

I finally faced the problem using maps instead of lists, because I needed to use a for_each argument and it doesn't accept a set of tuples as value.
If anyone knows a better approach, post it and I will be pleased of marking it as correct.
locals {
formatted_iam_roles = merge([ for member in local.default_iam_roles :
{ for pair in setproduct([member.name], member.roles) :
"${pair[0]}${pair[1]}" => { "name": pair[0], "role": pair[1] }
}
]...)
}
resource "google_project_iam_member" "team_access" {
for_each = local.formatted_iam_roles
project = var.project_id
member = each.value["name"]
role = each.value["role"]
}

Related

Convert from Tuple of strings to strings in terraform

I have an issue where I want to pass a list of vpc_ids to aws_route53_zone while getting the id from a couple of module calls and iterating it from the state file.
The out put format I am using is:
output "development_vpc_id" {
value = [for vpc in values(module.layout)[*] : vpc.id if vpc.environment == "development"]
description = "VPC id for development env"
}
where I get the output like:
"development_vpc_id": {
"value": [
"xxxx"
],
"type": [
"tuple",
[
"string"
]
]
},
instead I want to achieve below:
"developmemt_vpc_id": {
"value": "xxx",
"type": "string"
},
Can someone please help me with the same.
There isn't any automatic way to "convert" a sequence of strings into a single string, because you need to decide how you want to represent the multiple separate strings once you've reduced it into only a single string.
One solution would be to apply JSON encoding so that your output value is a string containing JSON array syntax:
output "development_vpc_id" {
value = jsonencode([
for vpc in values(module.layout)[*] : vpc.id
if vpc.environment == "development"
])
}
Another possibility is to concatenate all of the strings together with a particular character as a marker to separate each one, such as a comma:
output "development_vpc_id" {
value = join(",", [
for vpc in values(module.layout)[*] : vpc.id
if vpc.environment == "development"
])
}
If you expect that this list will always contain exactly one item -- that is, if each of your objects has a unique environment value -- then you could also tell Terraform about that assumption using the one function:
output "development_vpc_id" {
value = one([
for vpc in values(module.layout)[*] : vpc.id
if vpc.environment == "development"
])
}
In this case, Terraform will either return the one element of this sequence or will raise an error saying there are too many items in the sequence. The one function therefore acts as an assertion to help you detect if there's a bug which causes there to be more than one item in this list, rather than just silently discarding some of the items.

Terraform construct a list from map

I have a terraform map like below.
org_sub_accounts = [
{
"id" = "11111111111"
"type" = "test"
},
{
"id" = "22222222222"
"type" = "prod"
},
{
"id" = "33333333333"
"type" = "prod"
}
]
I want to create a list from this map by including the account id where the type is prod. So the output should be something like the below.
prod_accounts = ["22222222222","33333333333"]
Can someone please help? Was trying to figure this out for some time now.
Assuming that org_sub_accounts is defined as a local (modify answer accordingly if otherwise), then this can be constructed from a for expression lambda inside the list constructor:
[for account in local.org_sub_accounts : account.id if account.type == "prod"]
which returns the value:
[
"22222222222",
"33333333333",
]
which can be assigned to a prod_accounts as desired. Note this assumes your original structure is a list(object) as shown in the question, and therefore always contains the keys id and type.

Combine two arrays to create every possible combination with Azure Bicep [duplicate]

I want to create a resource for many regions, environments, apps, etc at once.
I'd like to do something like this:
param apps array = [
'app1'
'app2'
'app3'
]
param environments array = [
'alpha'
'beta'
]
param regions array = [
'ne'
'we'
'uks'
]
resource origin_group 'Microsoft.Cdn/profiles/origingroups#2021-06-01' = [ for region in regions: {
[ for env in environments: {
[ for app in apps: {
parent: profiles_global_fd_name_resource
name: '${env}-${region}-${app}-origin-group'
properties: {
loadBalancingSettings: {
sampleSize: 4
successfulSamplesRequired: 3
additionalLatencyInMilliseconds: 50
}
healthProbeSettings: {
probePath: '/'
probeRequestType: 'HEAD'
probeProtocol: 'Http'
probeIntervalInSeconds: 100
}
sessionAffinityState: 'Disabled'
}
}]
}]
}]
All docs mentioning nested loops talk about looping inside a resource to create many sub-resources. Not what I'm after. Perhaps another way would be to somehow merge all these arrays into a single array of objects of every possible iteration. Not sure where to start with that either.
Any help much appreciated.
This is not supported for the moment but it will (see Is there plans to support nested loop on resources?).
Using a little bit of math, you could achieve what you'd like (Not sure if you should):
param environments array = [ 'alpha', 'beta' ]
param regions array = [ 'ne', 'we', 'uks' ]
param apps array = [ 'app1', 'app2', 'app3' ]
// Setting some variables for clarity
var envCount = length(environments)
var regionCount = length(regions)
var appCount = length(apps)
// Setting the total number of combination
var originGroupCount = envCount * regionCount * appCount
// Iterate all possible combinations
output originGroupNames array = [for i in range(0, originGroupCount): {
name: '${environments[i / (regionCount * appCount) % envCount]}-${regions[i / appCount % regionCount]}-${apps[i % appCount]}-origin-group'
}]
this will output all possible combinations (I think) for origin group name.

Transform nested list values to include value from parent map

Is it possible to construct a data structure given the input variable below, where you append the name value to each value in the stages list;
environments = [
{
name = "preprod"
stages = ["blue", "green"]
},
{
name = "qat"
stages = ["blue", "green"]
}
]
so that I end up with a data structure that looks like this once flattened:
local.transformed = [
"preprod-blue",
"preprod-green",
"prod-blue",
"prod-green"
]
You can use flatten:
locals {
transformed = flatten([for val in var.environments: [
for color in val.stages:
"${val.name}-${color}"
]
])
}

how do I convert a Terraform map variable into a string?

I'm working on an tf plan what builds a json template and out of a map variable and I'm not quite sure how to use the existing looping, type, list functions to do the work. I know that I cannot pass lists or map to a data "template_file" so my thought was to build the string in a locals or null resource block and then pass that to the template
Variable
variable "boostrap_servers" {
type = map
default = {
"env01" : [
"k01.env01",
"k02.env01"
],
"env02" : [
"k01.env02"
]
}
Desired text
"connections": {
"env01": {
"properties": {
"bootstrap.servers": "k01.env01,k02.env01"
}
},
"env02": {
"properties": {
"bootstrap.servers": "k01.env02"
}
},
You may simply use the jsonencode function and list comprehension for this:
locals {
connections = jsonencode({
for cluster, servers in local.bootstrap_servers :
cluster => {
properties = {
"bootstrap.servers" = join(",", servers)
}
}
})
}
Ok, so the following works but there's a better question: why not just use the jsonencode function to build the json
locals {
clusters = [
for cluster, servers in var.boostrap_servers :
"{\"${cluster}\":{\"properties\":{\"bootstrap.servers\":\"${join(" ,", servers)}\"}}"]
connections = join(",", local.clusters)
}

Resources