|
| 1 | +# Part of the Terraform Dynamic Roles implementation, |
| 2 | +# this files reads in all the `aws-team` and `aws-team-roles` stacks |
| 3 | +# and extracts the relevant information. |
| 4 | + |
| 5 | +# The `utils_describe_stacks` data resources use the Cloud Posse Utils provider to describe Atmos stacks, and then |
| 6 | +# we merge the results into `local.all_team_vars`. This is the same as running the following locally: |
| 7 | +# ``` |
| 8 | +# atmos describe stacks --components=aws-teams,aws-team-roles --component-types=terraform --sections=vars |
| 9 | +# ``` |
| 10 | +# The result of these stack descriptions includes all metadata for the given components. For example, we now |
| 11 | +# can filter the result to find all stacks where either `aws-teams` or `aws-team-roles` are deployed. |
| 12 | +# Note that unlike in earlier implementations, we now expect `aws-team-roles` to be deployed in all accounts, |
| 13 | +# including the `identity` account. |
| 14 | +# |
| 15 | +# In particular, we can use this data to find the name of the account via `null-label` (defined by |
| 16 | +# `null-label.descriptor_formats.account_name`, typically `<tenant>-<stage>`) where team roles are deployed. |
| 17 | +# We then determine which roles are provisioned and which teams can access any given role in any particular account. |
| 18 | +# |
| 19 | +# `descriptor_formats.account_name` is typically defined in `stacks/orgs/NAMESPACE/_defaults.yaml`, and if not |
| 20 | +# defined, the stack name will default to `stage`.` |
| 21 | +# |
| 22 | +# If `namespace` is included in `descriptor_formats.account_name`, then we additionally filter to only stacks with |
| 23 | +# the same `namespace` as `module.this.namespace`. See `local.stack_namespace_index` and `local.stack_namespace_index` |
| 24 | +# |
| 25 | +# https://atmos.tools/cli/commands/describe/stacks/ |
| 26 | +# https://registry.terraform.io/providers/cloudposse/utils/latest/docs/data-sources/describe_stacks |
| 27 | + |
| 28 | +locals { |
| 29 | + # We would like to use code like this: |
| 30 | + # teams_stacks = local.dynamic_role_enabled ? { for k, v ... } : {} |
| 31 | + # but that generates an error: "Inconsistent conditional result types" |
| 32 | + # See https://github.com/hashicorp/terraform/issues/33303 |
| 33 | + # To work around this, we have "empty" values that depend on the condition. |
| 34 | + empty_map = { |
| 35 | + true = null |
| 36 | + false = {} |
| 37 | + } |
| 38 | + empty = local.empty_map[local.dynamic_role_enabled] |
| 39 | + |
| 40 | + # If a namespace is included with the stack name, only loop through stacks in the same namespace |
| 41 | + # zero-based index showing position of the namespace in the stack name |
| 42 | + stack_namespace_index = try(index(module.this.normalized_context.descriptor_formats.stack.labels, "namespace"), -1) |
| 43 | + stack_has_namespace = local.stack_namespace_index >= 0 |
| 44 | + # stack_account_map maps stack name to account short name |
| 45 | + stack_account_map = { for k, v in module.atmos : k => lookup(v.descriptors, "account_name", v.stage) } |
| 46 | + |
| 47 | + # ASSUMPTIONS: The stack pattern is the same for all accounts and uses the same delimiter as null-label |
| 48 | + |
| 49 | + # Part 1, get all the aws-teams information: |
| 50 | + # Get all the aws-teams stacks, optionally filtered by namespace, so that we only work in the current namespace. |
| 51 | + teams_stacks = local.dynamic_role_enabled ? { |
| 52 | + for k, v in yamldecode(data.utils_describe_stacks.teams[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false) |
| 53 | + } : local.empty |
| 54 | + |
| 55 | + # Extract components.terraform.aws-teams.vars. Key is the stack name, value is the vars. |
| 56 | + teams_vars = { for k, v in local.teams_stacks : k => v.components.terraform.aws-teams.vars if try(v.components.terraform.aws-teams.vars, null) != null } |
| 57 | + # Extract components.terraform.aws-teams.vars.teams_config, drop the stack name. |
| 58 | + teams_config = local.dynamic_role_enabled ? values(local.teams_vars)[0].teams_config : local.empty |
| 59 | + # Extract enabled team names from components.terraform.aws-teams.vars.teams_config |
| 60 | + team_names = [for k, v in local.teams_config : k if try(v.enabled, true)] |
| 61 | + # Convert team names to IAM role ARNs |
| 62 | + team_arns = { for team_name in local.team_names : team_name => format(local.iam_role_arn_templates[local.account_role_map.identity], team_name) } |
| 63 | + # Now we have local.team_arns which is a list of IAM role ARNs for each team. |
| 64 | + # This covers all the roles we have explicitly configured as teams. |
| 65 | + |
| 66 | + # Part 2, get all the aws-team-roles information: |
| 67 | + # Get all the aws-team-roles stacks, optionally filtered by namespace, so that we only work in the current namespace. |
| 68 | + team_roles_stacks = local.dynamic_role_enabled ? { |
| 69 | + for k, v in yamldecode(data.utils_describe_stacks.team_roles[0].output) : k => v if !local.stack_has_namespace || try(split(module.this.delimiter, k)[local.stack_namespace_index] == module.this.namespace, false) |
| 70 | + } : local.empty |
| 71 | + |
| 72 | + # Extract components.terraform.aws-team-roles.vars |
| 73 | + team_roles_vars = { for k, v in local.team_roles_stacks : k => v.components.terraform.aws-team-roles.vars if try(v.components.terraform.aws-team-roles.vars, null) != null } |
| 74 | + |
| 75 | + # Merge all the vars together so that for each stack name (the keys in the maps), |
| 76 | + # we can map it to the appropriate account short name, which we will use as keys in the final map. |
| 77 | + all_team_vars = merge(local.teams_vars, local.team_roles_vars) |
| 78 | + |
| 79 | +} |
| 80 | + |
| 81 | +data "utils_describe_stacks" "teams" { |
| 82 | + count = local.dynamic_role_enabled ? 1 : 0 |
| 83 | + |
| 84 | + components = ["aws-teams"] |
| 85 | + component_types = ["terraform"] |
| 86 | + sections = ["vars"] |
| 87 | +} |
| 88 | + |
| 89 | +data "utils_describe_stacks" "team_roles" { |
| 90 | + count = local.dynamic_role_enabled ? 1 : 0 |
| 91 | + |
| 92 | + components = ["aws-team-roles"] |
| 93 | + component_types = ["terraform"] |
| 94 | + sections = ["vars"] |
| 95 | +} |
| 96 | + |
| 97 | + |
| 98 | + |
| 99 | +module "atmos" { |
| 100 | + # local.all_team_vars is empty map when dynamic_role_enabled is false |
| 101 | + for_each = local.all_team_vars |
| 102 | + |
| 103 | + source = "cloudposse/label/null" |
| 104 | + version = "0.25.0" |
| 105 | + |
| 106 | + enabled = true |
| 107 | + namespace = lookup(each.value, "namespace", null) |
| 108 | + tenant = lookup(each.value, "tenant", null) |
| 109 | + environment = lookup(each.value, "environment", null) |
| 110 | + stage = lookup(each.value, "stage", null) |
| 111 | + name = lookup(each.value, "name", null) |
| 112 | + delimiter = lookup(each.value, "delimiter", null) |
| 113 | + attributes = lookup(each.value, "attributes", []) |
| 114 | + tags = lookup(each.value, "tags", {}) |
| 115 | + additional_tag_map = lookup(each.value, "additional_tag_map", {}) |
| 116 | + label_order = lookup(each.value, "label_order", []) |
| 117 | + regex_replace_chars = lookup(each.value, "regex_replace_chars", null) |
| 118 | + id_length_limit = lookup(each.value, "id_length_limit", null) |
| 119 | + label_key_case = lookup(each.value, "label_key_case", null) |
| 120 | + label_value_case = lookup(each.value, "label_value_case", null) |
| 121 | + descriptor_formats = lookup(each.value, "descriptor_formats", {}) |
| 122 | + labels_as_tags = lookup(each.value, "labels_as_tags", []) |
| 123 | +} |
0 commit comments