Skip to content

Commit 51b569d

Browse files
authored
Add most Premium features for gitlab_branch_protection (#556)
* Rebase and update go.mod Signed-off-by: Sune Keller <[email protected]> * Add most Premium features for gitlab_branch_protection Signed-off-by: Sune Keller <[email protected]> * Re-work modified schema for gitlab_branch_protection, and update docs Signed-off-by: Sune Keller <[email protected]> * Accommodate to name changes from library maintainer Signed-off-by: Sune Keller <[email protected]> * Remove invalid check for CE Signed-off-by: Sune Keller <[email protected]> * Revert back how merge_access_level and push_access_level are created Signed-off-by: Sune Keller <[email protected]> * Simplify expansion of arguments Signed-off-by: Sune Keller <[email protected]> * Remove several obsolete functions and make acceptance tests independent of implementation functions Signed-off-by: Sune Keller <[email protected]>
1 parent f47ca2e commit 51b569d

File tree

6 files changed

+778
-67
lines changed

6 files changed

+778
-67
lines changed

docs/resources/branch_protection.md

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1-
# gitlab\_branch_protection
1+
# gitlab\_branch\_protection
22

3-
This resource allows you to protect a specific branch by an access level so that the user with less access level cannot Merge/Push to the branch. GitLab EE features to protect by group or user are not supported.
3+
This resource allows you to protect a specific branch by an access level so that the user with less access level cannot Merge/Push to the branch.
4+
5+
-> The `allowed_to_push`, `allowed_to_merge` and `code_owner_approval_required` arguments require a GitLab Premium account or above.
46

57
## Example Usage
68

79
```hcl
810
resource "gitlab_branch_protection" "BranchProtect" {
9-
project = "12345"
10-
branch = "BranchProtected"
11-
push_access_level = "developer"
12-
merge_access_level = "developer"
11+
project = "12345"
12+
branch = "BranchProtected"
13+
push_access_level = "developer"
14+
merge_access_level = "developer"
15+
code_owner_approval_required = true
16+
allowed_to_push {
17+
user_id = 5
18+
}
19+
allowed_to_push {
20+
user_id = 521
21+
}
22+
allowed_to_merge {
23+
user_id = 15
24+
}
25+
allowed_to_merge {
26+
user_id = 37
27+
}
1328
}
1429
```
1530

@@ -21,8 +36,34 @@ The following arguments are supported:
2136

2237
* `branch` - (Required) Name of the branch.
2338

24-
* `push_access_level` - (Required) One of five levels of access to the project.
39+
* `push_access_level` - (Optional) One of five levels of access to the project. Valid values are: `no one`, `developer`, `maintainer`, `admin`.
40+
41+
* `merge_access_level` - (Optional) One of five levels of access to the project. Valid values are: `no one`, `developer`, `maintainer`, `admin`.
2542

26-
* `merge_access_level` - (Required) One of five levels of access to the project.
43+
* `allowed_to_push`, `allowed_to_merge` - (Optional) One or more `allowed_to_push`, `allowed_to_merge` blocks as defined below.
2744

2845
* `code_owner_approval_required` (Optional) Bool, defaults to false. Can be set to true to require code owner approval before merging.
46+
47+
---
48+
49+
An `allowed_to_push` or `allowed_to_merge` block supports the following arguments:
50+
51+
* `user_id` - (Required) The ID of a GitLab user allowed to perform the relevant action. Mutually exclusive with `group_id`.
52+
53+
* `group_id` - (Required) The ID of a GitLab group allowed to perform the relevant action. Mutually exclusive with `user_id`.
54+
55+
56+
## Attributes Reference
57+
58+
The following attributes are exported:
59+
60+
* The `allowed_to_push` and `allowed_to_merge` blocks export the `access_level_description` field, which contains a textual description of the access level, user or group allowed to perform the relevant action.
61+
62+
## Import
63+
64+
GitLab project freeze periods can be imported using an id made up of `project_id:branch`, e.g.
65+
66+
67+
```
68+
$ terraform import gitlab_branch_protection.BranchProtect "12345:main"
69+
```

gitlab/resource_gitlab_branch_protection.go

Lines changed: 148 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
package gitlab
22

33
import (
4-
"errors"
54
"fmt"
65
"log"
6+
"net/http"
77

88
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
99
gitlab "github.com/xanzy/go-gitlab"
1010
)
1111

12+
var (
13+
allowedToElem = &schema.Resource{
14+
Schema: map[string]*schema.Schema{
15+
"access_level": {
16+
Type: schema.TypeString,
17+
Computed: true,
18+
},
19+
"access_level_description": {
20+
Type: schema.TypeString,
21+
Computed: true,
22+
},
23+
"user_id": {
24+
Type: schema.TypeInt,
25+
Optional: true,
26+
},
27+
"group_id": {
28+
Type: schema.TypeInt,
29+
Optional: true,
30+
},
31+
},
32+
}
33+
)
34+
1235
func resourceGitlabBranchProtection() *schema.Resource {
1336
acceptedAccessLevels := make([]string, 0, len(accessLevelID))
1437

@@ -20,6 +43,9 @@ func resourceGitlabBranchProtection() *schema.Resource {
2043
Read: resourceGitlabBranchProtectionRead,
2144
Update: resourceGitlabBranchProtectionUpdate,
2245
Delete: resourceGitlabBranchProtectionDelete,
46+
Importer: &schema.ResourceImporter{
47+
State: schema.ImportStatePassthrough,
48+
},
2349
Schema: map[string]*schema.Schema{
2450
"project": {
2551
Type: schema.TypeString,
@@ -43,6 +69,8 @@ func resourceGitlabBranchProtection() *schema.Resource {
4369
Required: true,
4470
ForceNew: true,
4571
},
72+
"allowed_to_push": schemaAllowedTo(),
73+
"allowed_to_merge": schemaAllowedTo(),
4674
"code_owner_approval_required": {
4775
Type: schema.TypeBool,
4876
Optional: true,
@@ -55,47 +83,46 @@ func resourceGitlabBranchProtection() *schema.Resource {
5583
func resourceGitlabBranchProtectionCreate(d *schema.ResourceData, meta interface{}) error {
5684
client := meta.(*gitlab.Client)
5785
project := d.Get("project").(string)
58-
branch := gitlab.String(d.Get("branch").(string))
86+
branch := d.Get("branch").(string)
87+
88+
log.Printf("[DEBUG] create gitlab branch protection on branch %q for project %s", branch, project)
89+
90+
if d.IsNewResource() {
91+
existing, resp, err := client.ProtectedBranches.GetProtectedBranch(project, branch)
92+
if err != nil && resp.StatusCode != http.StatusNotFound {
93+
return fmt.Errorf("error looking up protected branch %q on project %q: %v", branch, project, err)
94+
}
95+
if resp.StatusCode != http.StatusNotFound {
96+
return fmt.Errorf("protected branch %q on project %q already exists: %+v", branch, project, *existing)
97+
}
98+
}
99+
59100
mergeAccessLevel := accessLevelID[d.Get("merge_access_level").(string)]
60101
pushAccessLevel := accessLevelID[d.Get("push_access_level").(string)]
61102
codeOwnerApprovalRequired := d.Get("code_owner_approval_required").(bool)
62103

63-
options := &gitlab.ProtectRepositoryBranchesOptions{
64-
Name: branch,
65-
MergeAccessLevel: &mergeAccessLevel,
104+
allowedToPush := expandBranchPermissionOptions(d.Get("allowed_to_push").(*schema.Set).List())
105+
allowedToMerge := expandBranchPermissionOptions(d.Get("allowed_to_merge").(*schema.Set).List())
106+
107+
pb, _, err := client.ProtectedBranches.ProtectRepositoryBranches(project, &gitlab.ProtectRepositoryBranchesOptions{
108+
Name: &branch,
66109
PushAccessLevel: &pushAccessLevel,
110+
MergeAccessLevel: &mergeAccessLevel,
111+
AllowedToPush: allowedToPush,
112+
AllowedToMerge: allowedToMerge,
67113
CodeOwnerApprovalRequired: &codeOwnerApprovalRequired,
68-
}
69-
70-
log.Printf("[DEBUG] create gitlab branch protection on %v for project %s", options.Name, project)
71-
72-
bp, _, err := client.ProtectedBranches.ProtectRepositoryBranches(project, options)
114+
})
73115
if err != nil {
74-
// Remove existing branch protection
75-
_, err = client.ProtectedBranches.UnprotectRepositoryBranches(project, *branch)
76-
if err != nil {
77-
return err
78-
}
79-
// Reprotect branch with updated values
80-
bp, _, err = client.ProtectedBranches.ProtectRepositoryBranches(project, options)
81-
if err != nil {
82-
return err
83-
}
116+
return fmt.Errorf("error protecting branch %q on project %q: %v", branch, project, err)
84117
}
85118

86-
d.SetId(buildTwoPartID(&project, &bp.Name))
87-
88-
if err := resourceGitlabBranchProtectionRead(d, meta); err != nil {
89-
return err
119+
if !pb.CodeOwnerApprovalRequired && codeOwnerApprovalRequired {
120+
return fmt.Errorf("feature unavailable: code owner approvals")
90121
}
91122

92-
// If the GitLab tier does not support the code owner approval feature, the resulting plan will be inconsistent.
93-
// We return an error because otherwise Terraform would report this inconsistency as a "bug in the provider" to the user.
94-
if codeOwnerApprovalRequired && !d.Get("code_owner_approval_required").(bool) {
95-
return errors.New("feature unavailable: code owner approvals")
96-
}
123+
d.SetId(buildTwoPartID(&project, &pb.Name))
97124

98-
return nil
125+
return resourceGitlabBranchProtectionRead(d, meta)
99126
}
100127

101128
func resourceGitlabBranchProtectionRead(d *schema.ResourceData, meta interface{}) error {
@@ -107,6 +134,7 @@ func resourceGitlabBranchProtectionRead(d *schema.ResourceData, meta interface{}
107134

108135
log.Printf("[DEBUG] read gitlab branch protection for project %s, branch %s", project, branch)
109136

137+
// Get protected branch by project ID/path and branch name
110138
pb, _, err := client.ProtectedBranches.GetProtectedBranch(project, branch)
111139
if err != nil {
112140
log.Printf("[DEBUG] failed to read gitlab branch protection for project %s, branch %s: %s", project, branch, err)
@@ -116,9 +144,31 @@ func resourceGitlabBranchProtectionRead(d *schema.ResourceData, meta interface{}
116144

117145
d.Set("project", project)
118146
d.Set("branch", pb.Name)
119-
d.Set("merge_access_level", accessLevel[pb.MergeAccessLevels[0].AccessLevel])
120-
d.Set("push_access_level", accessLevel[pb.PushAccessLevels[0].AccessLevel])
121-
d.Set("code_owner_approval_required", pb.CodeOwnerApprovalRequired)
147+
148+
pushAccessLevels := convertAllowedAccessLevelsToBranchAccessDescriptions(pb.PushAccessLevels)
149+
if len(pushAccessLevels) > 0 {
150+
if err := d.Set("push_access_level", pushAccessLevels[0].AccessLevel); err != nil {
151+
return fmt.Errorf("error setting push_access_level: %v", err)
152+
}
153+
}
154+
155+
mergeAccessLevels := convertAllowedAccessLevelsToBranchAccessDescriptions(pb.MergeAccessLevels)
156+
if len(mergeAccessLevels) > 0 {
157+
if err := d.Set("merge_access_level", mergeAccessLevels[0].AccessLevel); err != nil {
158+
return fmt.Errorf("error setting merge_access_level: %v", err)
159+
}
160+
}
161+
162+
if err := d.Set("allowed_to_push", convertAllowedToToBranchAccessDescriptions(pb.PushAccessLevels)); err != nil {
163+
return fmt.Errorf("error setting allowed_to_push: %v", err)
164+
}
165+
if err := d.Set("allowed_to_merge", convertAllowedToToBranchAccessDescriptions(pb.MergeAccessLevels)); err != nil {
166+
return fmt.Errorf("error setting allowed_to_merge: %v", err)
167+
}
168+
169+
if err := d.Set("code_owner_approval_required", pb.CodeOwnerApprovalRequired); err != nil {
170+
return fmt.Errorf("error setting code_owner_approval_required: %v", err)
171+
}
122172

123173
d.SetId(buildTwoPartID(&project, &pb.Name))
124174

@@ -172,3 +222,68 @@ func projectAndBranchFromID(id string) (string, string, error) {
172222
}
173223
return project, branch, err
174224
}
225+
226+
func expandBranchPermissionOptions(allowedTo []interface{}) []*gitlab.BranchPermissionOptions {
227+
result := make([]*gitlab.BranchPermissionOptions, 0)
228+
for _, v := range allowedTo {
229+
opt := &gitlab.BranchPermissionOptions{}
230+
if userID, ok := v.(map[string]interface{})["user_id"]; ok && userID != 0 {
231+
opt.UserID = gitlab.Int(userID.(int))
232+
}
233+
if groupID, ok := v.(map[string]interface{})["group_id"]; ok && groupID != 0 {
234+
opt.GroupID = gitlab.Int(groupID.(int))
235+
}
236+
result = append(result, opt)
237+
}
238+
return result
239+
}
240+
241+
func schemaAllowedTo() *schema.Schema {
242+
return &schema.Schema{
243+
Type: schema.TypeSet,
244+
Optional: true,
245+
ForceNew: true,
246+
Elem: allowedToElem,
247+
}
248+
}
249+
250+
type stateBranchAccessDescription struct {
251+
AccessLevel string `mapstructure:"access_level"`
252+
AccessLevelDescription string `mapstructure:"access_level_description"`
253+
GroupID int `mapstructure:"group_id,omitempty"`
254+
UserID int `mapstructure:"user_id,omitempty"`
255+
}
256+
257+
func convertAllowedAccessLevelsToBranchAccessDescriptions(descriptions []*gitlab.BranchAccessDescription) []stateBranchAccessDescription {
258+
result := make([]stateBranchAccessDescription, 0)
259+
260+
for _, description := range descriptions {
261+
if description.UserID != 0 || description.GroupID != 0 {
262+
continue
263+
}
264+
result = append(result, stateBranchAccessDescription{
265+
AccessLevel: accessLevel[description.AccessLevel],
266+
AccessLevelDescription: description.AccessLevelDescription,
267+
})
268+
}
269+
270+
return result
271+
}
272+
273+
func convertAllowedToToBranchAccessDescriptions(descriptions []*gitlab.BranchAccessDescription) []stateBranchAccessDescription {
274+
result := make([]stateBranchAccessDescription, 0)
275+
276+
for _, description := range descriptions {
277+
if description.UserID == 0 && description.GroupID == 0 {
278+
continue
279+
}
280+
result = append(result, stateBranchAccessDescription{
281+
AccessLevel: accessLevel[description.AccessLevel],
282+
AccessLevelDescription: description.AccessLevelDescription,
283+
UserID: description.UserID,
284+
GroupID: description.GroupID,
285+
})
286+
}
287+
288+
return result
289+
}

0 commit comments

Comments
 (0)