Terraform 0.12 nested for loops - nested-loops

I am trying to implement nested for loops using Terraform 0.12's new features in order to loop through AWS IAM users, each of which can have one or more policies attached. The variable used to represent this list is of type map(list(string)) and looks something like this:
{
"user 1" = [ "policy1", "policy2" ],
"user 2" = [ "policy1" ]
}
Getting the list of users to create is easy enough via keys(), but since there is currently no mechanism for nesting looped resource creation in Terraform, the policy attachments have to happen as a singular loop independent of each user. So, I am attempting to construct a list of user:policy associations from the map input that would look something like this based on the example above:
[
[ "user1", "policy1" ],
[ "user1", "policy2" ],
[ "user2", "policy1" ]
]
I am attempting construct that list and store it in a local variable like so, where var.iam-user-policy-map is the input map:
locals {
...
association-list = [
for user in keys(var.iam-user-policy-map):
[
for policy in var.iam-user-policy-map[user]:
[user, policy]
]
]
...
}
However, I am getting errors when attempting to access the values in that nested list. I am trying to access the user portion of the association with the reference local.association-list[count.index][0] and the policy with local.association-list[count.index][1], but on running terraform plan it errors out:
Error: Incorrect attribute value type
on main.tf line 27, in resource "aws_iam_user_policy_attachment" "test-attach":
27: user = local.association-list[count.index][0]
Inappropriate value for attribute "user": string required.
Error: Incorrect attribute value type
on main.tf line 27, in resource "aws_iam_user_policy_attachment" "test-attach":
27: user = local.association-list[count.index][0]
Inappropriate value for attribute "user": string required.
Error: Invalid index
on main.tf line 28, in resource "aws_iam_user_policy_attachment" "test-attach":
28: policy_arn = "arn:aws-us-gov:iam::aws:policy/${local.association-list[count.index][1]}"
|----------------
| count.index is 0
| local.association-list is tuple with 2 elements
The given key does not identify an element in this collection value.
Error: Invalid template interpolation value
on main.tf line 28, in resource "aws_iam_user_policy_attachment" "test-attach":
28: policy_arn = "arn:aws-us-gov:iam::aws:policy/${local.association-list[count.index][1]}"
|----------------
| count.index is 1
| local.association-list is tuple with 2 elements
Cannot include the given value in a string template: string required.
What am I doing wrong?

The for expression in your local value association-list is producing a list of list of lists of strings, but your references to it are treating it as a list of lists of strings.
To get the flattened representation you wanted, you can use the flatten function, but because it would otherwise group everything into a single flat list I'd recommend making the innermost value an object instead. (That will also make the references to it clearer.)
locals {
association-list = flatten([
for user in keys(var.iam-user-policy-map) : [
for policy in var.iam-user-policy-map[user] : {
user = user
policy = policy
}
]
])
}
The result of this expression will have the following shape:
[
{ user = "user1", policy = "policy1" },
{ user = "user1", policy = "policy2" },
{ user = "user2", policy = "policy2" },
]
Your references to it can then be in the following form:
user = local.association-list[count.index].user
policy_arn = "arn:aws-us-gov:iam::aws:policy/${local.association-list[count.index].policy}"

If you need a map for 'for_each', 'merge' is convenient.
variable "iam-user-policy-map" {
default = {
"user 1" = ["policy1", "policy2"],
"user 2" = ["policy1"]
}
}
locals {
association-map = merge([
for user, policies in var.iam-user-policy-map : {
for policy in policies :
"${user}-${policy}" => {
"user" = user
"policy" = policy
}
}
]...)
}
output "association-map" {
value = local.association-map
}
Outputs:
association-map = {
"user 1-policy1" = {
"policy" = "policy1"
"user" = "user 1"
}
"user 1-policy2" = {
"policy" = "policy2"
"user" = "user 1"
}
"user 2-policy1" = {
"policy" = "policy1"
"user" = "user 2"
}
}
Example for_each usage:
resource "null_resource" "echo" {
for_each = local.association-map
provisioner "local-exec" {
command = "echo 'policy - ${each.value.policy}, user - ${each.value.user}'"
}
}
https://github.com/hashicorp/terraform/issues/22263#issuecomment-969549347

I am not sure where I got this answer from, but this one worked for me.
locals {
schemas = [
"PRIVATE",
"PUBLIC",
"MY_SCHEMA",
]
privileges = [
"CREATE TABLE",
"CREATE VIEW",
"USAGE",
]
# Nested loop over both lists, and flatten the result.
schema_privileges = distinct(flatten([
for schema in local.schemas : [
for privilege in local.privileges : {
privilege = privilege
schema = schema
}
]
]))
}
resource "snowflake_schema_grant" "write_permissions" {
# We need a map to use for_each, so we convert our list into a map by adding a unique key:
for_each = { for entry in local.schema_privileges: "${entry.schema}.${entry.privilege}" => entry }
database_name = "MY_DATABASE"
privilege = each.value.privilege
roles = "DAVE"
schema_name = each.value.schema
}

Related

Terraform For Expression and more than one list

I want to use following for expression to create CloudWatch dashboard using TF 0.13.7
Variables:
variable "redis_replicas" {
type = list(map(string))
default = [
{
name = "test-eucentral1-redis-001"
region = "eu-central-1"
},
locals {
replicas = [for i in var.redis_replicas: i.name]
regions = [for i in var.redis_replicas: i.region]
resources
resource "aws_cloudwatch_dashboard" "cloudwatch_db_redis" {
dashboard_name = "Redis-Performance"
dashboard_body = jsonencode({
widgets : concat(local.body1)
})
}
body1 = [
{
height = 6
properties = {
metrics = [ for replica in local.replicas :
[
{
expression = "m1"
id = "e1"
},
], # getting error here
[
"AWS/ElastiCache",
"ClusterBasedCmds",
"CacheClusterId",
replica,
],
]
view = "timeSeries"
}
type = "metric"
}
]
Using
metrics = [for instance in var.redis_replicas : [
[],
[],
]
Plan works but apply is failing.
Error: Putting dashboard failed: InvalidParameterInput: The dashboard body is invalid, there are 50 validation errors:
[
{
"dataPath": "/widgets/9/properties/metrics/0/0",
"message": "Invalid metric field type, only \"String\" type is allowed"
},
Any idea how to combine those two?
It works if I do for in body but then I have different graphs for different replica.

How to define terraform aws_ami resource for Fedora Atomic Amazon Machine Image (ami)

I am trying to use terraform to get an aws_ami data resource as follows:
data "aws_ami" "fedora_atomic" {
most_recent = true
filter {
name = "name"
values = [
"ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"] <==== What to specify here?
}
filter {
name = "virtualization-type"
values = [
"hvm"]
}
owners = [
"099720109477"] <=== What's the owner id?
# Canonical
}
But I want to replace the above with the following image desription, which I found on the AWS console:
Fedora-Atomic-25-20170727.0.x86_64-us-east-1-HVM-standard-0 - ami-00035c7b
Question
How do I find the right values for the fields above i.e. what is the correct code for the above for a Fedora Atomic image?
I am struggling to find this information.
Many Thanks
Fedora Atomic has been EOL since 2019 and you won't find new AMIs but to answer your question, the owner is the Account ID and you can find it from the AWS Console
The name can be part of what is available in the description, ie Fedora-Atomic-25-
Combining them all
data "aws_ami" "fedora_atomic" {
most_recent = true
filter {
name = "name"
values = ["Fedora-Atomic-25-*"]
}
filter {
name = "virtualization-type"
values = [ "hvm"]
}
owners = ["125523088429"]
}
output "ami" {
value = data.aws_ami.fedora_atomic.id
}

Mail merge template : local variable not getting value in if..else

{ MERGEFIELD TableStart:Test}{ SET PLAN {MERGEFIELD Name}="XYZ" "1" "0"}}
{ MERGEFIELD TableEnd:Test }
{ IF { REF PLAN } = "1" "Pass" "Fail"}
In this example always getting result Fail, whether Name is "XYZ" or not.
can anyone suggest further ?
In your case in the SET field you should use IF field to evaluate condition. Please see the following field codes:
{ SET PLAN { IF {MERGEFIELD Name} = XYZ "1" "0"} }
{ IF { REF PLAN } = "1" "Pass" "Fail" }
After executing simple mail merge using the following code:
Document doc = new Document(#"C:\Temp\in.docx");
doc.MailMerge.Execute(new string[] { "Name" }, new string[] { "XYZ" });
doc.Save(#"C:\Temp\out.docx");
The resulting document has the following field codes:
{ SET PLAN XYZ = XYZ "1" "0"} }
{ IF { REF PLAN } = "1" "Pass" "Fail" }
which is properly evaluated with "Pass" text.
Also in MS Word document field whitespaces matters. See the screenshot from MS Word document on my side

Iterating through map of objects in terraform with route 53 records

I am trying to figure out how to read from additional values in Terraform using for / for_each using Terraform 0.12.26
dns.tfvars
mx = {
"mywebsite.org." = {
ttl = "3600"
records = [
"home.mywebsite.org.",
"faq.mywebsite.org."
]
}
"myotherwebsite.org." = {
ttl = "3600"
records = [
"home.myotherwebsite.org."
]
}
}
variables.tf
variable "mx" {
type = map(object({
ttl = string
records = set(string)
}))
}
mx.tf
locals {
mx_records = flatten([
for mx_key, mx in var.mx : [
for record in mx.records : {
mx_key = mx_key
record = record
ttl = mx.ttl
}]
])
}
resource "aws_route53_record" "mx_records" {
for_each = { for mx in local.mx_records : mx.mx_key => mx... }
zone_id = aws_route53_zone.zone.zone_id
name = each.key
type = "MX"
ttl = each.value.ttl
records = [
each.value.record
]
}
In mx.tf, I can comment out the second value, faq.mywebsite.org, and the code works perfectly. I cannot figure out how to set up my for loop and for each statements to get it to "loop" through the second value. The first error I had received stated below:
Error: Duplicate object key
on mx.tf line 13, in resource "aws_route53_record" "mx_records":
13: for_each = { for mx in local.mx_records : mx.mx_key => mx }
|----------------
| mx.mx_key is "mywebsite.org."
Two different items produced the key "mywebsite.org." in this 'for'
expression. If duplicates are expected, use the ellipsis (...) after the value
expression to enable grouping by key.
To my understanding, I do not have two duplicate values helping to form the key so I should not have to use the ellipsis, but I tried using the ellipsis anyway to see if it would apply properly. After adding on the ellipsis after the value expression, I got this error:
Error: Unsupported attribute
on mx.tf line 20, in resource "aws_route53_record" "mx_records":
20: each.value.record
|----------------
| each.value is tuple with 2 elements
This value does not have any attributes.
Any advice on this issue would be appreciated.
UPDATE
Error: [ERR]: Error building changeset: InvalidChangeBatch: [Tried to create resource record set [name='mywebsiteorg.', type='MX'] but it already exists]
status code: 400, request id: dadd6490-efac-47ac-be5d-ab8dad0f4a6c
It's trying to create the record, but it already created because of the first record in the list.
I think you could just construct a map of your objects with key being the index of mx_records list (note the idx being the index):
resource "aws_route53_record" "mx_records" {
for_each = { for idx, mx in local.mx_records : idx => mx }
zone_id = aws_route53_zone.zone.zone_id
name = each.value.mx_key
type = "MX"
ttl = each.value.ttl
records = [
each.value.record
]
}
The above for_each expressions changes your local.mx_records from list(objects) to map(objects), where the map key is idx, and the value is the original object.
Update:
I verified in Route53 and you can't duplicate codes. Thus may try using orginal mx variable:
resource "aws_route53_record" "mx_records" {
for_each = { for idx, mx in var.mx : idx => mx }
zone_id = aws_route53_zone.zone.zone_id
name = each.key
type = "MX"
ttl = each.value.ttl
records = each.value.records
}
Moreover, if you want to avoid flatten function and for loop local variable, you can access the object in the map as:
resource "aws_route53_record" "mx_records" {
for_each = var.mx
zone_id = aws_route53_zone.zone.zone_id
name = each.key
type = "MX"
ttl = each.value["ttl"]
records = each.value["records"]
}

RavenDb 4: Check if a string of an array of strings exists in different array of strings

I am trying to filter my activities based on user roles. The manager is only allowed to see the activities that contain the role Manager. See below for more information.
Data:
var user = new User
{
Roles = [
"Manager"
]
}
var activities = new List<Activity>
{
new Activity
{
Description = "My First Activity",
Roles = [
"Admin",
"Manager"
]
},
new Activity
{
Description = "My Second Activity",
Roles = [
"Admin"
]
},
new Activity
{
Description = "My Third Activity",
Roles = [
"Manager",
"Client"
]
},
}
Query that filters the activity
var filtered = await context.Query<Activity>()
.Where(x => x.Roles.Any(y => user.Roles.Contains(y)))
.ToListAsync();
Expected result of filtered:
[
{
new Activity
{
Description = "My First Activity",
Roles = [
"Admin",
"Manager"
]
}
new Activity
{
Description = "My Third Activity",
Roles = [
"Manager",
"Client"
]
},
}
]
Error from query
System.InvalidOperationException : Can't extract value from expression of type: Parameter
I am getting an error, so obviously I am doing something wrong. Is this the correct way or is there something better?
This requires RavenDB to do computation during query, try this, instead:
.Where(x => x.Roles.In(user.Roles)) (might need to add a using statement for the In extension method, Raven.Client.Linq, IIRC).

Resources