Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ moved {
}
```

## Nested subgroups

A group can be up to three levels deep: a top-level group, a subgroup, and a group in that subgroup.
For a group in a subgroup, set `parent` to the key of the subgroup, `<parent>/<name>`:

```yaml
groups:
- name: infra
- name: terraform
parent: infra
- name: terraform-aws-modules
parent: infra/terraform # creates infra/terraform/terraform-aws-modules
```

`parent` uses the subgroup's `name`, not its `path`. Keep `name` and `path` the same, so `parent` is also the full path of the parent group, and a project can use it as `namespace`.

Groups whose parent is a top-level group stay in `gitlab_group.subgroups`. Groups whose parent is a subgroup go to `gitlab_group.nested_subgroups`. Thus the addresses of current groups do not change.
A `parent` deeper than a subgroup (`a/b/c`) fails validation.

See [examples/terraform/groups/infra.yaml](examples/terraform/groups/infra.yaml) and [examples/terraform/projects/infra_projects.yaml](examples/terraform/projects/infra_projects.yaml).

## Outputs

The module provides the following outputs:
Expand Down
28 changes: 28 additions & 0 deletions examples/terraform/groups/infra.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
groups:
- name: infra
create: true
settings:
visibility: private
description: "Infrastructure top-level group"

- name: terraform
create: true
parent: infra
settings:
path: terraform
visibility: internal
description: "Terraform modules mirrored from upstream"
share_groups:
- share_group_id: "roles/project-sre-dev"
group_access: maintainer

# A group under a subgroup. parent is the key of the subgroup, "<parent>/<name>",
# with the subgroup's name, not its path. Keep name and path the same to make
# parent equal to the full path of the parent group.
- name: terraform-aws-modules
create: true
parent: infra/terraform
settings:
path: terraform-aws-modules
visibility: internal
description: "Mirrors of github.com/terraform-aws-modules"
8 changes: 8 additions & 0 deletions examples/terraform/projects/infra_projects.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
projects:
# A project in a nested subgroup: namespace is the full path of the group.
- name: terraform-aws-vpc
create: true
namespace: "infra/terraform/terraform-aws-modules"
visibility: internal
description: "Mirror of github.com/terraform-aws-modules/terraform-aws-vpc"
settings: {}
130 changes: 100 additions & 30 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ resource "gitlab_group" "parent_groups" {
}
}

# Create Subgroups
# Create Subgroups (parent is a top-level group)
resource "gitlab_group" "subgroups" {
for_each = {
for group in var.gitlab_groups :
"${group.parent}/${group.name}" => group
if contains(keys(group), "parent")
if contains(keys(group), "parent") && !strcontains(lookup(group, "parent", ""), "/")
}

name = each.value.name
Expand Down Expand Up @@ -130,6 +130,84 @@ resource "gitlab_group" "subgroups" {
}
}

# Create Nested Subgroups (parent is a subgroup, e.g. parent = "infra/terraform").
# parent must match the subgroup key "<parent>/<name>", which uses the subgroup's
# name, not its path.
resource "gitlab_group" "nested_subgroups" {
for_each = {
for group in var.gitlab_groups :
"${group.parent}/${group.name}" => group
if length(split("/", lookup(group, "parent", ""))) == 2
}

name = each.value.name
path = lookup(each.value.settings, "path", each.value.name)
description = lookup(each.value.settings, "description", null)
visibility_level = lookup(each.value.settings, "visibility", "private")

parent_id = gitlab_group.subgroups[each.value.parent].id

# Group settings
auto_devops_enabled = lookup(each.value.settings, "auto_devops_enabled", false)
lfs_enabled = lookup(each.value.settings, "lfs_enabled", true)
mentions_disabled = lookup(each.value.settings, "mentions_disabled", false)
project_creation_level = lookup(each.value.settings, "project_creation_level", "maintainer")
request_access_enabled = lookup(each.value.settings, "request_access_enabled", false)
require_two_factor_authentication = lookup(each.value.settings, "require_two_factor_authentication", false)
share_with_group_lock = lookup(each.value.settings, "share_with_group_lock", false)
subgroup_creation_level = lookup(each.value.settings, "subgroup_creation_level", "owner")
two_factor_grace_period = lookup(each.value.settings, "two_factor_grace_period", 48)
avatar = lookup(each.value.settings, "avatar", null)
avatar_hash = lookup(each.value.settings, "avatar_hash", null)
emails_enabled = lookup(each.value.settings, "emails_enabled", null)
extra_shared_runners_minutes_limit = lookup(each.value.settings, "extra_shared_runners_minutes_limit", null)
ip_restriction_ranges = lookup(each.value.settings, "ip_restriction_ranges", null)
membership_lock = lookup(each.value.settings, "membership_lock", null)
prevent_forking_outside_group = lookup(each.value.settings, "prevent_forking_outside_group", null)
shared_runners_minutes_limit = lookup(each.value.settings, "shared_runners_minutes_limit", null)
shared_runners_setting = lookup(each.value.settings, "shared_runners_setting", null)
wiki_access_level = lookup(each.value.settings, "wiki_access_level", null)

# Replace deprecated default_branch_protection with new block
default_branch_protection_defaults {
allow_force_push = lookup(each.value.settings, "allow_force_push", false)
allowed_to_merge = lookup(each.value.settings, "allowed_to_merge", ["maintainer"])
allowed_to_push = lookup(each.value.settings, "allowed_to_push", ["maintainer"])
developer_can_initial_push = lookup(each.value.settings, "developer_can_initial_push", false)
}

dynamic "push_rules" {
for_each = try(each.value.settings.push_rules, [])
iterator = rule
content {
author_email_regex = try(rule.value.author_email_regex, null)
branch_name_regex = try(rule.value.branch_name_regex, null)
commit_committer_check = try(rule.value.commit_committer_check, null)
commit_message_negative_regex = try(rule.value.commit_message_negative_regex, null)
commit_message_regex = try(rule.value.commit_message_regex, null)
deny_delete_tag = try(rule.value.deny_delete_tag, null)
file_name_regex = try(rule.value.file_name_regex, null)
max_file_size = try(rule.value.max_file_size, null)
member_check = try(rule.value.member_check, null)
prevent_secrets = try(rule.value.prevent_secrets, null)
reject_unsigned_commits = try(rule.value.reject_unsigned_commits, null)
}
}
lifecycle {
create_before_destroy = true
}
}

locals {
# IDs of every group this module manages, keyed the same way as the resources:
# "<name>" for top-level groups, "<parent>/<name>" for subgroups and nested subgroups.
group_ids = merge(
{ for key, group in gitlab_group.parent_groups : key => group.id },
{ for key, group in gitlab_group.subgroups : key => group.id },
{ for key, group in gitlab_group.nested_subgroups : key => group.id },
)
}

# Create GitLab Group Access Tokens
resource "gitlab_group_access_token" "this" {
for_each = merge([
Expand All @@ -146,7 +224,7 @@ resource "gitlab_group_access_token" "this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]

name = each.value.token.name
scopes = each.value.token.scopes
Expand Down Expand Up @@ -177,7 +255,7 @@ resource "gitlab_group_variable" "access_token_this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
key = each.value.variable_name
value = sensitive(gitlab_group_access_token.this[each.key].token)
protected = true
Expand All @@ -202,7 +280,7 @@ resource "gitlab_group_badge" "this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
link_url = each.value.badge.link_url
image_url = each.value.badge.image_url
name = each.value.badge.name
Expand All @@ -224,7 +302,7 @@ resource "gitlab_group_custom_attribute" "this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
key = each.value.attribute.key
value = each.value.attribute.value
}
Expand All @@ -245,7 +323,7 @@ resource "gitlab_group_label" "this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
name = each.value.label.name
description = lookup(each.value.label, "description", null)
color = each.value.label.color
Expand All @@ -268,7 +346,7 @@ resource "gitlab_group_epic_board" "this" {
]...)

name = each.value.board.name
group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]

dynamic "lists" {
for_each = length(each.value.board.lists) > 0 ? toset(each.value.board.lists) : []
Expand All @@ -295,7 +373,7 @@ resource "gitlab_group_hook" "this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
url = each.value.hook.url
confidential_issues_events = lookup(each.value.hook, "confidential_issues_events", false)
confidential_note_events = lookup(each.value.hook, "confidential_note_events", false)
Expand Down Expand Up @@ -333,7 +411,7 @@ resource "gitlab_group_issue_board" "this" {
]...)

name = each.value.board.name
group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
labels = each.value.board.labels
dynamic "lists" {
for_each = length(each.value.board.lists) > 0 ? toset(each.value.board.lists) : []
Expand Down Expand Up @@ -373,7 +451,7 @@ resource "gitlab_group_membership" "this" {
}
]...)

group_id = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group_id = local.group_ids[each.value.group_name]
access_level = each.value.member.access_level
user_id = data.gitlab_user.membership_user[each.value.member.user_id].id
expires_at = lookup(each.value.member, "expires_at", null)
Expand All @@ -398,7 +476,7 @@ resource "gitlab_group_protected_environment" "this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
deploy_access_levels = each.value.env.deploy_access_levels
environment = each.value.env.environment
approval_rules = each.value.env.approval_rules
Expand All @@ -421,7 +499,7 @@ resource "gitlab_group_saml_link" "this" {
}
]...)

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.group_name].id : gitlab_group.subgroups[each.value.group_name].id
group = local.group_ids[each.value.group_name]
access_level = each.value.saml_link.access_level
saml_group_name = each.value.saml_link.saml_group_name
}
Expand Down Expand Up @@ -486,7 +564,7 @@ locals {
resource "gitlab_group_variable" "this_sensitive" {
for_each = local.group_variables_sensitive

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.owner].id : gitlab_group.subgroups[each.value.owner].id
group = local.group_ids[each.value.owner]
key = each.value.variable.key
value = sensitive(each.value.variable.value)
protected = lookup(each.value.variable, "protected", false)
Expand All @@ -501,7 +579,7 @@ resource "gitlab_group_variable" "this_sensitive" {
resource "gitlab_group_variable" "this" {
for_each = local.group_variables

group = each.value.parent == null ? gitlab_group.parent_groups[each.value.owner].id : gitlab_group.subgroups[each.value.owner].id
group = local.group_ids[each.value.owner]
key = each.value.variable.key
value = each.value.variable.value
protected = lookup(each.value.variable, "protected", false)
Expand Down Expand Up @@ -598,6 +676,7 @@ locals {
managed_groups_by_path = merge(
{ for name, group in gitlab_group.parent_groups : name => [{ group_id = group.id }] },
{ for path, group in gitlab_group.subgroups : path => [{ group_id = group.id }] },
{ for path, group in gitlab_group.nested_subgroups : path => [{ group_id = group.id }] },
)

# Group by name for groups, allowing for duplicates (keeps the [0] indexing used
Expand Down Expand Up @@ -640,8 +719,7 @@ resource "gitlab_project" "this" {

namespace_id = try(
local.exists_groups[each.value.namespace][0].group_id,
gitlab_group.subgroups[each.value.namespace].id,
gitlab_group.parent_groups[each.value.namespace].id,
local.group_ids[each.value.namespace],
null
)

Expand Down Expand Up @@ -1685,7 +1763,7 @@ resource "gitlab_group_deploy_token" "this" {
: "group-${group.name}-${token.name}" # Fallback to group name only if no parent
) => {
deploy_token = token
entity_id = contains(keys(group), "parent") ? gitlab_group.subgroups["${group.parent}/${group.name}"].id : gitlab_group.parent_groups[group.name].id
entity_id = local.group_ids[contains(keys(group), "parent") ? "${group.parent}/${group.name}" : group.name]
}
}
]...)
Expand All @@ -1704,16 +1782,8 @@ resource "gitlab_group_share_group" "this" {
group.parent != null ? "${group.parent}/${group.group_id}-${group.share_group_id}" : "${group.group_id}-${group.share_group_id}" => group
}

group_id = contains(keys(gitlab_group.parent_groups), each.value.group_id) ? gitlab_group.parent_groups[each.value.group_id].id : (
contains(keys(gitlab_group.subgroups), each.value.group_id)
? gitlab_group.subgroups[each.value.group_id].id
: gitlab_group.subgroups["${each.value.parent}/${each.value.group_id}"].id
)
share_group_id = contains(keys(gitlab_group.parent_groups), each.value.share_group_id) ? gitlab_group.parent_groups[each.value.share_group_id].id : (
contains(keys(gitlab_group.subgroups), each.value.share_group_id)
? gitlab_group.subgroups[each.value.share_group_id].id
: gitlab_group.subgroups["${each.value.parent}/${each.value.share_group_id}"].id
)
group_access = each.value.group_access
expires_at = lookup(each.value, "expires_at", null)
group_id = try(local.group_ids[each.value.group_id], local.group_ids["${each.value.parent}/${each.value.group_id}"])
share_group_id = try(local.group_ids[each.value.share_group_id], local.group_ids["${each.value.parent}/${each.value.share_group_id}"])
group_access = each.value.group_access
expires_at = lookup(each.value, "expires_at", null)
}
5 changes: 5 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ output "subgroups_group_ids" {
value = [for group in gitlab_group.subgroups : group.id]
}

output "nested_subgroups_group_ids" {
description = "List of created nested GitLab subgroup IDs."
value = [for group in gitlab_group.nested_subgroups : group.id]
}

output "project_ids" {
description = "List of created GitLab project IDs."
value = [for project in gitlab_project.this : project.id]
Expand Down
Loading
Loading