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.
Related
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.
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"]
}
In my base module I have this:
dynamic "settings" {
for_each = var.settings
content {
type = settings.value["type"]
operator = settings.value["operator"]
property = settings.value["property"]
target = settings.value["target"]
path = settings.value["path"]
}
}
path is a conditional field sometimes present sometimes not and is actually a block. In the calling module:
settings = [
{ "operator" = "lessThan", "type" = "responseTime", "target" = 10000 },
{ "operator" = "is", "type" = "statusCode", "target" = 200 },
{ "operator" = "is", "property" = "allow", "type" = "header", "target" = "true" },
{ "operator" = "validate", "type" = "body", "path" = { "operator" = "contains", "targetValue" = "9754389", "jsonPath" = "141234" } }
]
Now when I try to plan this I get following error:
An argument named "path" is not expected here. Did you mean to define a block of type "path"?
Question is, how can I define a block field inside a dynamic block?
I tried using a nested dynamic block and working with for-each but path can only have one instance, so it gives error that can't have more than one instance.
I haven't created a block inside a dynamic block before, so I'm just guessing here. Have you tried something like this:
dynamic "settings" {
for_each = var.settings
content {
type = settings.value["type"]
operator = settings.value["operator"]
property = settings.value["property"]
target = settings.value["target"]
path {
operator = settings.value["path"]["operator"]
targetValue = settings.value["path"]["targetValue"]
jsonPath = settings.value["path"]["jsonPath"]
}
}
}
As written, that will require that "path" exists in all of your settings entries, but if that works you might be able to fiddle around with some other approach to make it optional.
Just a next-step suggestion, I'm interested to know how you get on.
I'm struggling with this interpolation, I have variables below like.
primary = ["foo.dev","all.bar.com"]
secondary = ["module.foo.dev","*.foo.dev","all.bar.com"]
I want my output to be
{
"foo.dev" = ["module.foo.dev","*foo.dev"]
"all.bar.com" = ["all.bar.com"]
}
Using Terraform 0.12.20
I tried in terraform console , I'm not able to achieve my desired output. Is there any easy solution?
[for i in ["foo.dev","all.bar.com"]: map(i,[for j in ["module.foo.dev","*foo.dev","all.bar.com"]: replace(j,i,"")!=j == true ? j : 0])]
[
{
"foo.dev" = [
"module.foo.dev",
"*foo.dev",
"0",
]
},
{
"all.bar.com" = [
"0",
"0",
"all.bar.com",
]
},
]
You can do this with the following expression:
{ for i in ["foo.dev", "all.bar.com"] : i => [for j in ["module.foo.dev", "*foo.dev", "all.bar.com"] : j if can(regex(i, j))] }
This results in the following value:
{
"all.bar.com" = [
"all.bar.com",
]
"foo.dev" = [
"module.foo.dev",
"*foo.dev",
]
}
which is exactly what you requested.
The iterative explanation follows:
First, specify the type is a map:
{}
Now, we should iterate over the elements in the first list:
{ for i in ["foo.dev", "all.bar.com"] : }
Next, we assign the i value to the key of the map, and initialize a list for the value:
{ for i in ["foo.dev", "all.bar.com"] : i => [] }
However, we need to assign the values from the elements in the second list to the list for the key of the resulting map (the below example is sub-optimal because we do not need a for expression, but it is illustrative for the next step):
{ for i in ["foo.dev", "all.bar.com"] : i => [for j in ["module.foo.dev", "*foo.dev", "all.bar.com"] : j] }
Finally, we want to filter the elements in the list value based on the key. We can use the can function outside the regex function to return a boolean based on whether there is a match. We use this for the conditional on whether the element of the second list should be added to the list in the value:
{ for i in ["foo.dev", "all.bar.com"] : i => [for j in ["module.foo.dev", "*foo.dev", "all.bar.com"] : j if can(regex(i, j))] }
and we have arrived at the solution.
I have below terraform configuration for cognito client:
data "aws_cognito_user_pools" "re_user_pool" {
name = "${var.cognito_user_pool_name}"
}
resource "aws_cognito_user_pool_client" "app_client" {
name = "re-app-client"
user_pool_id = data.aws_cognito_user_pools.re_user_pool.id
depends_on = [data.aws_cognito_user_pools.re_user_pool]
explicit_auth_flows = ["USER_PASSWORD_AUTH"]
prevent_user_existence_errors = "ENABLED"
allowed_oauth_flows_user_pool_client = true
allowed_oauth_flows = ["code"]
allowed_oauth_scopes = ["phone", "openid", "email", "profile", "aws.cognito.signin.user.admin"]
supported_identity_providers = ["COGNITO", "Google"]
callback_urls = ["https://scnothzsf0.execute-api.ap-southeast-2.amazonaws.com/staging/signup"]
}
I references the cognito user pool which already exists on AWS. The error happens on the line user_pool_id = data.aws_cognito_user_pools.re_user_pool.id when it uses the user pool id in aws_cognito_user_pool_client.
I will get the error
Error: Error creating Cognito User Pool Client: InvalidParameterException: 1 validation error detected: Value 're-user' at 'userPoolId' failed to satisfy constraint: Member must satisfy regular expression pattern: [\w-]+_[0-9a-zA-Z]+
on infra/cognito.tf line 5, in resource "aws_cognito_user_pool_client" "app_client":
5: resource "aws_cognito_user_pool_client" "app_client" {`
It seems the format of the ID is not correct. I have read this document https://www.terraform.io/docs/providers/aws/d/cognito_user_pools.html and it has a reference attribute ids - The list of cognito user pool ids.. I wonder why it gives a list of user pool id. How can I reference this ID?
I also tried to reference it as user_pool_id = data.aws_cognito_user_pools.re_user_pool.ids[0] but got an error:
Error: Invalid index
on infra/cognito.tf line 8, in resource "aws_cognito_user_pool_client" "app_client":
8: user_pool_id = data.aws_cognito_user_pools.re_user_pool.ids[0]
This value does not have any indices.
The re_user_pool referenced above is defined here:
resource "aws_cognito_user_pool" "re_user_pool" {
name = "re-user"
}
I came across your question while working through this same problem. I see the question is several months old, but I'm still going to add an answer for anyone else that ends up here like I did.
First, the solution is to convert the ids value from a set to a list via the tolist function and then access it as you would any terraform list.
Caveat: In my case, I have ensured I only have one user pool for a given name, but you could get multiple user pools if you haven't followed this convention. This solution will not be a complete solution for that situation, but perhaps it will still point in the right direction.
Example code:
data "aws_cognito_user_pools" "test" {
name = "a_name"
}
output "test" {
value = "${tolist(data.aws_cognito_user_pools.test.ids)[0]"
}
Second, how I arrived at it:
I added an output block so I could see what I was working with and I commented out the problematic lines in my terraform file so I could successfully execute terraform apply. Next I ran terraform apply followed by terraform output --json (note: the apply must be successful for output to have the latest values).
Example temporary output block:
output "test" {
value = "${data.aws_cognito_user_pools.test}" // output top-level object for debugging
}
Relevant terraform apply output:
test = {
"arns" = [
"<redacted>",
]
"id" = "a_name"
"ids" = [
"us-east-1_<redacted>",
]
"name" = "a_name"
}
Relevant terraform output --json output:
"test": {
"sensitive": false,
"type": [
"object",
{
"arns": [
"set",
"string"
],
"id": "string",
"ids": [
"set",
"string"
],
"name": "string"
}
],
"value": {
"arns": [
"<redacted>"
],
"id": "a_name",
"ids": [
"us-east-1_<redacted>"
],
"name": "a_name"
}
}
As you can see, the ids portion is a set of type string. I decided to try converting ids to a list to see if I could then access the 0 index and it worked. I feel like this could be a terraform bug, but I haven't filed an issue yet.