Produce maps from list of strings of a map

Hello,

I have been pulling my hair on this for a couple of days now, I was wondering if someone could come up with a clever way to produce something like:

Outputs:

association-map = {
  "policy1_user1" = [
    "policy1",
     "user1"
  ]
  "policy2_user1" = [
    "policy2",
    "user1"
  ]
  "policy2_user2" = [ 
    "policy2",
    "user2" 
  ]
}

From:

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

The actual use case behind this is that I would like my team to be able to define GCP roles and members in two distinct lists to begin with:

variable admin_roles = {
  default = [
    "roles/resourcemanager.folderAdmin",
    "roles/resourcemanager.folderIamAdmin",
  ]
}

variable admin_members = {
  default = [
    "user:user1@example.com",
    "group:group1@example.com",
  ]
}

I then create a bindings map out of the two lists and feed it to a google_folder_iam_binding resource with for_each:

locals {
  admin_bindings = {
    for role in var.admin_roles:
      role => var.admin_members
  }
}

resource "google_folder_iam_binding" "binding" {
  for_each = local.admin_bindings
 
  folder = "folder_1234"
  members = each.value
  role = each.key
}

Which works as intended and allows us to remove/add members and roles anywhere in the original lists without triggering a delete/create of the resulting resources.

But I then realized I did not always want to be authoritative on the roles bindings, so I embarked on trying to produce a map I could feed to a google_folder_iam_member resource with for_each:

locals {
  admin_bindings_additive = {
    # insert magic to transform local.admin_bindings into a map of maps linking 
    # each role with each of its member in the following format:
    #   "roles/resourcemanager.folderAdmin_user:user1" = [
    #     "roles/resourcemanager.folderAdmin",
    #     "user:user1@example.com" 
    #   ]
    #   "roles/resourcemanager.folderAdmin_group:group1@example.com" = [
    #     "roles/resourcemanager.folderAdmin",
    #     "group:group1@example.com"
    #   ]
    #   "roles/resourcemanager.folderIamAdmin_user:user1@example.com" = [
    #     "roles/resourcemanager.folderIamAdmin",
    #     "user:user1@example.com"
    #   ]
    #   "roles/resourcemanager.folderIamAdmin_group:group1@example.com" = [
    #     "roles/resourcemanager.folderIamAdmin",
    #     "group:group1@example.com"
    #   ]
    # }
  }
}

resource "google_folder_iam_member" "member" {
  for_each = local.admin_bindings_additive

  folder = "folder_1234"
  member = each.value[1]
  role = each.value[0]
}

But failed miserably, for obvious reasons described in https://github.com/hashicorp/terraform/issues/22263.

I am not ready to give up yet, so I am hoping someone smarter could point me in a better direction.

Hi @sleterrier!

It looks like your base need here is to find all of the combinations of policy and user given in your variable value. We can start by solving just that problem, not worrying too much about how the result is shaped:

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

The local.user_policy_pairs value would then be the following, with your example input:

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

If having these grouped together into unique pairs is all you need and the exact data structure isn’t important then it might be better to use these objects to represent the pairs, since it’ll be clearer elsewhere in the configuration where it’s used what purpose each of the attributes has, rather than having to remember that element zero is the policy and element one is the user:

output "association-map" {
  value = {
    for obj in local.user_policy_pairs : "${obj.policy}_${obj.user}" => obj
  }
}

Your later each references would then be like each.value.user rather than each.value[1].

But if you do need that map-of-lists structure then you can produce it by transforming the previous result:

output "association-map" {
  value = {
    for obj in local.user_policy_pairs : "${obj.policy}_${obj.user}" => [obj.policy, obj.user]
  }
}

Please note that usual Terraform style is to use names that consist only of lowercase letters, numbers, and underscores. Terraform supports uppercase letters and dashes in identifiers only to make it more convenient to write names that will be sent to remote systems that may have different conventions. Your input variable would ideally be named iam_policy_users_map instead, and likewise your output value be association_map.

1 Like

Hi @apparentlymart!

Now that you broke it down for me, I feel stupid I didn’t think of transforming the data structure twice. I had stopped after the first flattened structure. It did not even occur to me I was so close to the end result.

output "association-map" {
  value = {
    for obj in local.user_policy_pairs : "${obj.policy}_${obj.user}" => obj
  }
}

is exactly what I needed to feed the pairs to a for_each.

Thank you so much, you’re my new hero :heart:

Hi @apparentlymart,

I’m having as similar sort of issue and tried you approach, but still having issue. Can you please take a look and suggest where I’m going wrong.

1 Like