leters
leters

Reputation: 401

Terraform 0.12 - Produce map/object from nested for loop

As a follow up to Terraform 0.12 nested for loops. I am trying to produce an object out of a nested loop but failing miserably :(

How would you go about producing:

  Outputs:

  association-list = {
    "policy1" = "user1"
    "policy2" = "user1"
    "policy2" = "user2"
  }

From:

iam-policy-users-map = {
  "policy1" = [ "user1" ]
  "policy2" = [ "user1", "user2" ]
}

I have tried many variations of:

variable iam-policy-users-map {
  default = {
    "policy1" = [ "user1" ]
    "policy2" = [ "user1", "user2" ]
  }
}

locals {
  association-map = merge({
    for policy, users in var.iam-policy-users-map : {
      for user in users : {
        policy => user
      }
    }
  })

output association-map {
  value = local.association-map
}

with zero success so far. Only managed to get the following depending on the variation:

Error: Invalid 'for' expression. Extra characters after the end of the 'for' expression.

Error: Missing attribute value. Expected an attribute value, introduced by an equals sign ("=").

Error: Invalid 'for' expression. Key expression is required when building an object.

Error: Missing key/value separator. Expected an equals sign ("=") to mark the beginning of the attribute value.

For reference, the following code is however capable of producing a list of maps:

variable iam-policy-users-map {
  default = {
    "policy1" = [ "user1" ]
    "policy2" = [ "user1", "user2" ]
  }
}

locals {    
  association-list = flatten([
    for policy, users in var.iam-policy-users-map : [
      for user in users : {
        user   = user
        policy = policy
      }
    ]
  ])
}

output association-list {
  value = local.association-list
}

Outputs:

association-list = [
  {
    "policy" = "policy1"
    "user" = "user1"
  },   {
    "policy" = "policy2"
    "user" = "user1"
  },   {
    "policy" = "policy2"
    "user" = "user2"
  }, 
]

Upvotes: 19

Views: 50149

Answers (2)

jawspeak
jawspeak

Reputation: 975

I was able to do something like this too. Sharing if these terraform list comprehension ideas help someone else trying to mutate to make a derivative map. (I originally found this and the linked issue above via this error Invalid 'for' expression: Key expression is not valid when building a tuple..

My goal was to have resources in the tfstate to not be a list because those would be hard to do tfstate operations on them ...[0], ...[1] are not clear what resources they are.

I had a variable like this which represented users to create some default folders in some s3 bucket.

locals { 
  #really in a tfvars file
  bucket_users = 
    "testuser-abc" = {
        make_extra_dirs = ["this-dir", "that-dir-2"]
    }
}


# Create extra folders for non-standard use cases per their configuration.
locals {
    # This is the easy way to get an array of extra directories to create
    # But, we don't want to create it or the `tfstate list` command shows non-helpful indices.
    # e.g. 
    # aws_s3_bucket_object.loop_user_folders_extra_customized_folders["0"]
    # aws_s3_bucket_object.loop_user_folders_extra_customized_folders["1"]
    # aws_s3_bucket_object.loop_user_folders_extra_customized_folders["2"]
    # So use the below trick
    users_with_extra_dirs_flat = flatten([
        for username, user_data in local.bucket_users : [
            for extra_dir in user_data["make_extra_dirs"]: {
                username = username,
                extra_dir = extra_dir,
                unique_key = format("%s--%s", username, extra_dir)
            }
        ] if length(user_data["make_extra_dirs"]) > 0
    ])

    # This is much easier to reason about as it creates nice `tfstate list` 
    # output in case we need to do any future refactorings.
    # e.g.
    # aws_s3_bucket_object.loop_user_folders_extra_customized_folders["testuser-abc--this-dir"]
    # aws_s3_bucket_object.loop_user_folders_extra_customized_folders["testuser-abc--that-dir-2"]
    users_with_extra_dirs_hash_makes_more_maintainable_tfstate_keys_vs_array = {
        for each in local.users_with_extra_dirs_flat : each.unique_key => { username = each.username, extra_dir = each.extra_dir }
    }
}

resource "aws_s3_bucket_object" "loop_user_folders_extra_customized_folders" {
    for_each = local.users_with_extra_dirs_hash_makes_more_maintainable_tfstate_keys_vs_array
    # lesser way for_each = {for idx, value in local.users_with_extra_dirs_flat : idx => value }

    bucket = "my-example-bucket"
    acl = "private"
    key = "somepath/${each.value.username}/${each.value.extra_dir}/" # Note this must end in a "/" to make that appear as 'directory'
    source = "/dev/null"
}

There are probably several ways to clean this up further, though having an intermediate variable and then transforming that helped some with explaining what is happening, and debugging.

Upvotes: 0

leters
leters

Reputation: 401

A partial answer can be found at https://github.com/hashicorp/terraform/issues/22263. Long story short: this was a foolish attempt to begin with, a map cannot contain duplicate keys.

I am however still interested in understanding how a map of maps could be produced from a nested for loop. See second code example above, producing a list of maps.

EDIT: a full answer was given on the github issue linked above.

"This is (obviously) a useless structure, but I wanted to illustrate that it is possible:

locals {
  association-list = {
    for policy, users in var.iam-policy-users-map:
      policy => {      // can't have the nested for expression before the key!
        for u in users:
           policy => u...
      }
  }
}

Outputs:

association-list = {
  "policy1" = {
    "policy1" = [
      "user1",
    ]
  }
  "policy2" = {
    "policy2" = [
      "user1",
      "user2",
    ]
  }
}

"

Upvotes: 21

Related Questions