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
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ A comprehensive Terraform module for creating and managing GitLab groups with fu
## Features

- **Complete GitLab Group Management** - Full support for all `gitlab_group` resource features
- **Group Membership Management** - Add users to groups with different access levels (owner, maintainer, developer, reporter, guest)
- **Advanced Security Settings** - Two-factor authentication, IP restrictions, and push rules
- **Access Control** - Granular permissions for project/subgroup creation and membership
- **CI/CD Integration** - Shared runners configuration and minutes management
Expand All @@ -30,6 +31,62 @@ module "basic_group" {
}
```

### Group with Members

```hcl
module "team_group" {
source = "sudo-terraform-modules/groups/gitlab"
version = "0.2.0"

name = "backend-team"
path = "backend-team"
description = "Backend development team"

# Add team members with different access levels
owners = [101, 102] # User IDs for group owners
maintainers = [201, 202, 203] # User IDs for maintainers
developers = [301, 302, 303, 304] # User IDs for developers
reporters = [401] # User IDs for reporters
guests = [501, 502] # User IDs for guests
}
```

### Advanced Member Management

```hcl
module "project_group" {
source = "sudo-terraform-modules/groups/gitlab"
version = "0.2.0"

name = "project-alpha"
path = "project-alpha"
description = "Project Alpha team"

# Simple member lists
owners = [100]
developers = [200, 201, 202]

# Advanced member configuration with expiration dates
members = {
contractor_1 = {
user_id = 300
access_level = "developer"
expires_at = "2024-12-31"
}
contractor_2 = {
user_id = 301
access_level = "developer"
expires_at = "2024-12-31"
}
external_auditor = {
user_id = 400
access_level = "reporter"
expires_at = "2024-06-30"
}
}
}
```

### Advanced Group with Security Features

```hcl
Expand Down Expand Up @@ -157,6 +214,12 @@ module "cicd_group" {
| parent_id | The ID of the parent group (creates nested group) | `number` | `null` |
| project_creation_level | Who can create projects (noone, owner, maintainer, developer, administrator) | `string` | `"maintainer"` |
| subgroup_creation_level | Who can create subgroups (owner, maintainer) | `string` | `"owner"` |
| owners | List of user IDs to add as owners | `list(number)` | `[]` |
| maintainers | List of user IDs to add as maintainers | `list(number)` | `[]` |
| developers | List of user IDs to add as developers | `list(number)` | `[]` |
| reporters | List of user IDs to add as reporters | `list(number)` | `[]` |
| guests | List of user IDs to add as guests | `list(number)` | `[]` |
| members | Map of users with custom access levels and optional expiration dates | `map(object)` | `{}` |
| require_two_factor_authentication | Require 2FA for all group members | `bool` | `false` |
| two_factor_grace_period | 2FA enforcement grace period (hours) | `number` | `48` |
| lfs_enabled | Enable Large File Storage for projects | `bool` | `true` |
Expand All @@ -169,6 +232,7 @@ module "cicd_group" {

For a complete list of all input variables, see the [variables.tf](./variables.tf) file. The module supports all GitLab group features including:

- **Membership Management**: owners, maintainers, developers, reporters, guests, members (with expiration)
- **Access Control**: membership_lock, share_with_group_lock, prevent_forking_outside_group
- **Features**: auto_devops_enabled, wiki_access_level, emails_enabled, mentions_disabled
- **CI/CD**: shared_runners_minutes_limit, extra_shared_runners_minutes_limit
Expand Down Expand Up @@ -202,6 +266,14 @@ For a complete list of all input variables, see the [variables.tf](./variables.t
| cicd_settings | CI/CD and shared runners configuration |
| feature_settings | Feature and integration settings |

### Membership Outputs

| Name | Description |
|------|-------------|
| members | All group members with their access levels |
| member_count | Total number of members added to the group |
| member_ids | List of all user IDs that are members of the group |

### Convenience Outputs

| Name | Description |
Expand Down
70 changes: 70 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# GITLAB GROUP RESOURCE
# =====================

# LOCAL VARIABLES FOR MEMBERSHIP MANAGEMENT
# ==========================================
locals {
# Convert simple user ID lists to sets for efficient management
owner_set = toset([for id in var.owners : tostring(id)])
maintainer_set = toset([for id in var.maintainers : tostring(id)])
developer_set = toset([for id in var.developers : tostring(id)])
reporter_set = toset([for id in var.reporters : tostring(id)])
guest_set = toset([for id in var.guests : tostring(id)])
}

resource "gitlab_group" "this" {
# REQUIRED ATTRIBUTES
# ==================
Expand Down Expand Up @@ -102,3 +114,61 @@ resource "gitlab_group" "this" {
}
}
}

# GITLAB GROUP MEMBERSHIP RESOURCES
# ==================================

# Custom members with flexible configuration (supports expiration dates)
resource "gitlab_group_membership" "custom_members" {
for_each = var.members

group_id = gitlab_group.this.id
user_id = each.value.user_id
access_level = each.value.access_level
expires_at = each.value.expires_at
}

# Owner members
resource "gitlab_group_membership" "owners" {
for_each = local.owner_set

group_id = gitlab_group.this.id
user_id = tonumber(each.value)
access_level = "owner"
}

# Maintainer members
resource "gitlab_group_membership" "maintainers" {
for_each = local.maintainer_set

group_id = gitlab_group.this.id
user_id = tonumber(each.value)
access_level = "maintainer"
}

# Developer members
resource "gitlab_group_membership" "developers" {
for_each = local.developer_set

group_id = gitlab_group.this.id
user_id = tonumber(each.value)
access_level = "developer"
}

# Reporter members
resource "gitlab_group_membership" "reporters" {
for_each = local.reporter_set

group_id = gitlab_group.this.id
user_id = tonumber(each.value)
access_level = "reporter"
}

# Guest members
resource "gitlab_group_membership" "guests" {
for_each = local.guest_set

group_id = gitlab_group.this.id
user_id = tonumber(each.value)
access_level = "guest"
}
63 changes: 63 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,66 @@ output "group_url_slug" {
description = "The URL slug for the group (same as path, provided for clarity)."
value = gitlab_group.this.path
}

# GROUP MEMBERSHIP INFORMATION
# ============================

output "members" {
description = "All group members with their access levels."
value = merge(
{ for k, v in gitlab_group_membership.custom_members : k => {
user_id = v.user_id
access_level = v.access_level
expires_at = v.expires_at
} },
{ for k, v in gitlab_group_membership.owners : "owner_${k}" => {
user_id = v.user_id
access_level = v.access_level
expires_at = null
} },
{ for k, v in gitlab_group_membership.maintainers : "maintainer_${k}" => {
user_id = v.user_id
access_level = v.access_level
expires_at = null
} },
{ for k, v in gitlab_group_membership.developers : "developer_${k}" => {
user_id = v.user_id
access_level = v.access_level
expires_at = null
} },
{ for k, v in gitlab_group_membership.reporters : "reporter_${k}" => {
user_id = v.user_id
access_level = v.access_level
expires_at = null
} },
{ for k, v in gitlab_group_membership.guests : "guest_${k}" => {
user_id = v.user_id
access_level = v.access_level
expires_at = null
} }
)
}

output "member_count" {
description = "Total number of members added to the group."
value = (
length(gitlab_group_membership.custom_members) +
length(gitlab_group_membership.owners) +
length(gitlab_group_membership.maintainers) +
length(gitlab_group_membership.developers) +
length(gitlab_group_membership.reporters) +
length(gitlab_group_membership.guests)
)
}

output "member_ids" {
description = "List of all user IDs that are members of the group."
value = distinct(concat(
[for m in gitlab_group_membership.custom_members : m.user_id],
[for m in gitlab_group_membership.owners : m.user_id],
[for m in gitlab_group_membership.maintainers : m.user_id],
[for m in gitlab_group_membership.developers : m.user_id],
[for m in gitlab_group_membership.reporters : m.user_id],
[for m in gitlab_group_membership.guests : m.user_id]
))
}
96 changes: 96 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,98 @@ variable "push_rules" {
}
}

# GROUP MEMBERSHIP CONFIGURATION
# ===============================

variable "members" {
description = "Map of users to add to the group with their access levels. Keys can be any unique identifier, values are objects with user_id and access_level."
type = map(object({
user_id = number
access_level = string
expires_at = optional(string)
}))
default = {}

validation {
condition = alltrue([
for member in var.members : contains(
["no one", "minimal", "guest", "reporter", "developer", "maintainer", "owner"],
member.access_level
)
])
error_message = "Access level must be one of: no one, minimal, guest, reporter, developer, maintainer, owner."
}

validation {
condition = alltrue([
for member in var.members : member.user_id > 0
])
error_message = "User ID must be a positive integer."
}

validation {
condition = alltrue([
for member in var.members : member.expires_at == null || can(regex("^\\d{4}-\\d{2}-\\d{2}$", member.expires_at))
])
error_message = "Expiration date must be in YYYY-MM-DD format if specified."
}
}

variable "owners" {
description = "List of user IDs to add as owners of the group."
type = list(number)
default = []

validation {
condition = alltrue([for id in var.owners : id > 0])
error_message = "All user IDs must be positive integers."
}
}

variable "maintainers" {
description = "List of user IDs to add as maintainers of the group."
type = list(number)
default = []

validation {
condition = alltrue([for id in var.maintainers : id > 0])
error_message = "All user IDs must be positive integers."
}
}

variable "developers" {
description = "List of user IDs to add as developers of the group."
type = list(number)
default = []

validation {
condition = alltrue([for id in var.developers : id > 0])
error_message = "All user IDs must be positive integers."
}
}

variable "reporters" {
description = "List of user IDs to add as reporters of the group."
type = list(number)
default = []

validation {
condition = alltrue([for id in var.reporters : id > 0])
error_message = "All user IDs must be positive integers."
}
}

variable "guests" {
description = "List of user IDs to add as guests of the group."
type = list(number)
default = []

validation {
condition = alltrue([for id in var.guests : id > 0])
error_message = "All user IDs must be positive integers."
}
}

# ==============================================================================
# VARIABLE GROUPS SUMMARY
# ==============================================================================
Expand All @@ -319,6 +411,10 @@ variable "push_rules" {
# - request_access_enabled, prevent_forking_outside_group
# - project_creation_level, subgroup_creation_level
#
# MEMBERSHIP VARIABLES:
# - members: Map of users with custom access levels and expiration dates
# - owners, maintainers, developers, reporters, guests: Simplified user ID lists
#
# OPERATIONAL VARIABLES:
# - shared_runners_setting, shared_runners_minutes_limit
# - lfs_enabled, auto_devops_enabled, wiki_access_level
Expand Down