Create new map from a nested structure with for loops

This is for setting up our users and teams in github with terraform.
(provider github 2.8.0)

Basically I want to define all users with one structure like:

gh_users = {
    user_one = {
        username = "user1",
        main_role = "member",
        teams  = {
            team1 = {
                role = "member"
            }
        }
    },
    user_two = {
        username  = "user2",
        main_role = "admin",
        teams     = {
            team1 = {
                role = "maintainer"
            },
           team2 = {
               role = "maintainer"
           }
       }
   }

}

Using maps to be able to reference all steps by names and use for_each.

Im now trying to create resources, and for creating users it works fine:

resource "github_membership" "users" {
      for_each = var.gh_users

      username = each.key
      role            = each.value.main_role
}

But when I want to assign the members to groups (resource github_team_membership) it is not as straightforward…
For this I need “team_ids”, “usernames”, and “user roles”, all which is defined in the structure above, but I just cant wrap my head around how.
Im trying to create another local map team_members with team-user-role information with a nested for loop, and then doing a for_each over that map, like this:

resource "github_team_membership" "members" {
    for_each = local.team_members

    team_id     = each.value.team_name
    username = each.value.username
    role            = each.value.role
}

locals {    
    team_members = {
        for user, user_info in var.gh_users: [
            for team, team_data in user_info.teams: {
                "${user}-${team}" = {
                    "team_name" = team,
                    "username"    = user_info.username,
                    "role"               = team_data.role
                }
            }
        ]
    }
}

Here I get: “Key expression is required when building an object.”
I have tried a few other ways but all fails so if somebody can show me here I would be glad! (I have full control over the data structure so changes there can be made as well…)

3 Likes

Hi @tomasbackman!

It looks like you had the right idea here! Indeed, the requirement is to construct a collection that has one element per instance you want to create, which in this case means a collection of “team memberships” – the join of users and teams.

The reason for the error message you saw is that you have a mapping-oriented for ({ for ... }) with a sequence-oriented for ([for ...]) inside it, but you’ve put the key expression "${user}-${team}" on the inner one that is constructing a sequence, rather than the outer one that is constructing a mapping.

A common way to get the result you are looking for here is to make your intermediate value a list of “team membership” objects, which makes it possible to use flatten to build it, and then project it into the map you need just in time inside the for_each expression. For example:

locals {
  team_members = flatten([
    for user, user_info in var.gh_users : [
      for team, deam_data in user_info.teams : {
        team_name = team
        username  = user_info.username
        role      = team_data.role
      }
    ]
  ])
}

resource "github_team_membership" "members" {
    for_each = {
      for tm in local.team_members : "${user}-${team}" => tm
    }

    team_id  = each.value.team_name
    username = each.value.username
    role     = each.value.role
}

Another nice side-effect of this formulation is that it keeps the expression for how the unique keys are constructed directly inside the resource block, so a future reader can hopefully quickly understand that this resource will produce instances with keys like user1-team1 without having to refer to expressions elsewhere in the module.

5 Likes

Thank you @apparentlymart that worked much better! Especially with that link to the description under “flatten”. (But maybe it could be considered to put that very useful tip somewhere under the documentation for “for_each” expression? which I think would make it much easier to find…)

And a note (small correction) for anyone else reading this, the "${user}-${team}" => tm expression should be: "${tm.user}-${tm.team}" => tm

Now I’m stuck on next step actually getting/using the ids for the github teams instead of the names… but that is more of a github_provider issue and should be solvable… so continuing the work. =)

4 Likes

Hi @tomasbackman!

Funnily enough I noticed yesterday that the links to that flatten example and the setproduct example were both a bit buried in the documentation (just a passing remark in the "Using Expressions for for_each" section) and so I pushed up an edit that makes them a bit more prominent. That’ll show up in the published documentation with the forthcoming 0.13.0 release.

Sorry for the mistakes with user rather than tm.user, etc! I’m glad you were able to figure out what I meant to say. :confounded:

2 Likes