Skip to content

Commit af6f192

Browse files
felixlutfelixlutnickfloyd
authored
feat: support team organization role assignment (#2322)
* add octokit sdk client * add github_team_org_role_assignment resource * add tests for github_team_org_role_assignment * formatting * use go-github for listing all team role assignments for a role * implement proper import logic * add docs * test adding .gitattributes to make vendor pr changes not slow down the UI * test adding .gitattributes to make vendor pr changes not slow down the UI * test adding .gitattributes to make vendor pr changes not slow down the UI * remove .gitattributes, seems like it had no effect * add break iteration over teams assigned to organization role * org role team assignment no longer use go-sdk * purge go-sdk completely * support either role_name or role_id * update docs * resources should follow convention from gh api (org role before team) * update docs * finalize the renaming * fix importer * go back to only allowing github_slug and role_id * linting fix * revert go.mod changes --------- Co-authored-by: felixlut <[email protected]> Co-authored-by: Nick Floyd <[email protected]>
1 parent a4ceb1f commit af6f192

File tree

4 files changed

+342
-0
lines changed

4 files changed

+342
-0
lines changed

github/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ func Provider() *schema.Provider {
162162
"github_organization_custom_properties": resourceGithubOrganizationCustomProperties(),
163163
"github_organization_project": resourceGithubOrganizationProject(),
164164
"github_organization_security_manager": resourceGithubOrganizationSecurityManager(),
165+
"github_organization_role_team_assignment": resourceGithubOrganizationRoleTeamAssignment(),
165166
"github_organization_ruleset": resourceGithubOrganizationRuleset(),
166167
"github_organization_settings": resourceGithubOrganizationSettings(),
167168
"github_organization_webhook": resourceGithubOrganizationWebhook(),
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"log"
6+
"strconv"
7+
8+
"github.com/google/go-github/v66/github"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func resourceGithubOrganizationRoleTeamAssignment() *schema.Resource {
13+
return &schema.Resource{
14+
Create: resourceGithubOrganizationRoleTeamAssignmentCreate,
15+
Read: resourceGithubOrganizationRoleTeamAssignmentRead,
16+
Delete: resourceGithubOrganizationRoleTeamAssignmentDelete,
17+
Importer: &schema.ResourceImporter{
18+
StateContext: schema.ImportStatePassthroughContext,
19+
},
20+
21+
Schema: map[string]*schema.Schema{
22+
"team_slug": {
23+
Type: schema.TypeString,
24+
Required: true,
25+
Description: "The GitHub team slug.",
26+
ForceNew: true,
27+
},
28+
"role_id": {
29+
Type: schema.TypeString,
30+
Required: true,
31+
Description: "The GitHub organization role id",
32+
ForceNew: true,
33+
},
34+
},
35+
}
36+
}
37+
38+
func resourceGithubOrganizationRoleTeamAssignmentCreate(d *schema.ResourceData, meta interface{}) error {
39+
err := checkOrganization(meta)
40+
if err != nil {
41+
return err
42+
}
43+
44+
client := meta.(*Owner).v3client
45+
orgName := meta.(*Owner).name
46+
ctx := context.Background()
47+
48+
teamSlug := d.Get("team_slug").(string)
49+
roleIDString := d.Get("role_id").(string)
50+
51+
roleID, err := strconv.ParseInt(roleIDString, 10, 64)
52+
if err != nil {
53+
return err
54+
}
55+
56+
_, err = client.Organizations.AssignOrgRoleToTeam(ctx, orgName, teamSlug, roleID)
57+
if err != nil {
58+
return err
59+
}
60+
61+
d.SetId(buildTwoPartID(teamSlug, roleIDString))
62+
return resourceGithubOrganizationRoleTeamAssignmentRead(d, meta)
63+
}
64+
65+
func resourceGithubOrganizationRoleTeamAssignmentRead(d *schema.ResourceData, meta interface{}) error {
66+
err := checkOrganization(meta)
67+
if err != nil {
68+
return err
69+
}
70+
71+
client := meta.(*Owner).v3client
72+
ctx := context.Background()
73+
orgName := meta.(*Owner).name
74+
75+
teamSlug, roleIDString, err := parseTwoPartID(d.Id(), "team_slug", "role_id")
76+
if err != nil {
77+
return err
78+
}
79+
80+
roleID, err := strconv.ParseInt(roleIDString, 10, 64)
81+
if err != nil {
82+
return err
83+
}
84+
85+
// There is no api for checking a specific team role assignment, so instead we iterate over all teams assigned to the role
86+
// go-github pagination (https://github.com/google/go-github?tab=readme-ov-file#pagination)
87+
options := &github.ListOptions{
88+
PerPage: 100,
89+
}
90+
var foundTeam *github.Team
91+
for {
92+
teams, resp, err := client.Organizations.ListTeamsAssignedToOrgRole(ctx, orgName, roleID, options)
93+
if err != nil {
94+
return err
95+
}
96+
97+
for _, team := range teams {
98+
if team.GetSlug() == teamSlug {
99+
foundTeam = team
100+
break
101+
}
102+
103+
}
104+
105+
if resp.NextPage == 0 {
106+
break
107+
}
108+
options.Page = resp.NextPage
109+
}
110+
111+
if foundTeam == nil {
112+
log.Printf("[WARN] Removing team organization role association %s from state because it no longer exists in GitHub", d.Id())
113+
d.SetId("")
114+
return nil
115+
}
116+
117+
if err = d.Set("team_slug", teamSlug); err != nil {
118+
return err
119+
}
120+
if err = d.Set("role_id", roleIDString); err != nil {
121+
return err
122+
}
123+
124+
return nil
125+
}
126+
127+
func resourceGithubOrganizationRoleTeamAssignmentDelete(d *schema.ResourceData, meta interface{}) error {
128+
err := checkOrganization(meta)
129+
if err != nil {
130+
return err
131+
}
132+
133+
client := meta.(*Owner).v3client
134+
orgName := meta.(*Owner).name
135+
ctx := context.Background()
136+
137+
teamSlug, roleIDString, err := parseTwoPartID(d.Id(), "team_slug", "role_id")
138+
if err != nil {
139+
return err
140+
}
141+
142+
roleID, err := strconv.ParseInt(roleIDString, 10, 64)
143+
if err != nil {
144+
return err
145+
}
146+
147+
_, err = client.Organizations.RemoveOrgRoleFromTeam(ctx, orgName, teamSlug, roleID)
148+
if err != nil {
149+
return err
150+
}
151+
152+
return nil
153+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
9+
)
10+
11+
func TestAccGithubOrganizationRoleTeamAssignment(t *testing.T) {
12+
13+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
14+
15+
// Using the predefined roles since custom roles are a strictly Enterprise feature ((https://github.blog/changelog/2024-07-10-pre-defined-organization-roles-that-grant-access-to-all-repositories/))
16+
githubPredefinedRoleMapping := make(map[string]string)
17+
githubPredefinedRoleMapping["all_repo_read"] = "8132"
18+
githubPredefinedRoleMapping["all_repo_triage"] = "8133"
19+
githubPredefinedRoleMapping["all_repo_write"] = "8134"
20+
githubPredefinedRoleMapping["all_repo_maintain"] = "8135"
21+
githubPredefinedRoleMapping["all_repo_admin"] = "8136"
22+
23+
t.Run("creates repo assignment without error", func(t *testing.T) {
24+
25+
teamSlug := fmt.Sprintf("tf-acc-test-team-repo-%s", randomID)
26+
config := fmt.Sprintf(`
27+
resource "github_team" "test" {
28+
name = "%s"
29+
description = "test"
30+
}
31+
resource github_organization_role_team_assignment "test" {
32+
team_slug = github_team.test.slug
33+
role_id = "%s"
34+
}
35+
`, teamSlug, githubPredefinedRoleMapping["all_repo_read"])
36+
37+
check := resource.ComposeTestCheckFunc(
38+
resource.TestCheckResourceAttr(
39+
"github_organization_role_team_assignment.test", "id", fmt.Sprintf("%s:%s", teamSlug, githubPredefinedRoleMapping["all_repo_read"]),
40+
),
41+
resource.TestCheckResourceAttr(
42+
"github_organization_role_team_assignment.test", "team_slug", teamSlug,
43+
),
44+
resource.TestCheckResourceAttr(
45+
"github_organization_role_team_assignment.test", "role_id", githubPredefinedRoleMapping["all_repo_read"],
46+
),
47+
)
48+
49+
testCase := func(t *testing.T, mode string) {
50+
resource.Test(t, resource.TestCase{
51+
PreCheck: func() { skipUnlessMode(t, mode) },
52+
Providers: testAccProviders,
53+
Steps: []resource.TestStep{
54+
{
55+
Config: config,
56+
Check: check,
57+
},
58+
},
59+
})
60+
}
61+
62+
t.Run("with an anonymous account", func(t *testing.T) {
63+
t.Skip("anonymous account not supported for this operation")
64+
})
65+
66+
t.Run("with an individual account", func(t *testing.T) {
67+
t.Skip("individual account not supported for this operation")
68+
})
69+
70+
t.Run("with an organization account", func(t *testing.T) {
71+
testCase(t, organization)
72+
})
73+
})
74+
75+
// More tests can go here following the same format...
76+
t.Run("create and re-creates role assignment without error", func(t *testing.T) {
77+
78+
teamSlug := fmt.Sprintf("tf-acc-test-team-repo-%s", randomID)
79+
configs := map[string]string{
80+
"before": fmt.Sprintf(`
81+
resource "github_team" "test" {
82+
name = "%s"
83+
description = "test"
84+
}
85+
resource github_organization_role_team_assignment "test" {
86+
team_slug = github_team.test.slug
87+
role_id = "%s"
88+
}
89+
`, teamSlug, githubPredefinedRoleMapping["all_repo_read"]),
90+
"after": fmt.Sprintf(`
91+
resource "github_team" "test" {
92+
name = "%s"
93+
description = "test"
94+
}
95+
resource github_organization_role_team_assignment "test" {
96+
team_slug = github_team.test.slug
97+
role_id = "%s"
98+
}
99+
`, teamSlug, githubPredefinedRoleMapping["all_repo_write"]),
100+
}
101+
102+
checks := map[string]resource.TestCheckFunc{
103+
"before": resource.ComposeTestCheckFunc(
104+
resource.TestCheckResourceAttr(
105+
"github_organization_role_team_assignment.test", "role_id", githubPredefinedRoleMapping["all_repo_read"],
106+
),
107+
),
108+
"after": resource.ComposeTestCheckFunc(
109+
resource.TestCheckResourceAttr(
110+
"github_organization_role_team_assignment.test", "role_id", githubPredefinedRoleMapping["all_repo_write"],
111+
),
112+
),
113+
}
114+
115+
testCase := func(t *testing.T, mode string) {
116+
resource.Test(t, resource.TestCase{
117+
PreCheck: func() { skipUnlessMode(t, mode) },
118+
Providers: testAccProviders,
119+
Steps: []resource.TestStep{
120+
{
121+
Config: configs["before"],
122+
Check: checks["before"],
123+
},
124+
{
125+
Config: configs["after"],
126+
Check: checks["after"],
127+
},
128+
},
129+
})
130+
}
131+
132+
t.Run("with an anonymous account", func(t *testing.T) {
133+
t.Skip("anonymous account not supported for this operation")
134+
})
135+
136+
t.Run("with an individual account", func(t *testing.T) {
137+
t.Skip("individual account not supported for this operation")
138+
})
139+
140+
t.Run("with an organization account", func(t *testing.T) {
141+
testCase(t, organization)
142+
})
143+
})
144+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
layout: "github"
3+
page_title: "GitHub: github_organization_role_team_assignment"
4+
description: |-
5+
Manages the associations between teams and organization roles.
6+
---
7+
8+
# github_organization_role_team_assignment
9+
10+
This resource manages relationships between teams and organization roles
11+
in your GitHub organization. This works on predefined roles, and custom roles, where the latter is an Enterprise feature.
12+
13+
Creating this resource assigns the role to a team.
14+
15+
The organization role and team must both belong to the same organization
16+
on GitHub.
17+
18+
## Example Usage
19+
20+
```hcl
21+
resource "github_team" "test-team" {
22+
name = "test-team"
23+
}
24+
25+
resource "github_organization_role_team_assignment" "test-team-role-assignment" {
26+
team_slug = github_team.test-team.slug
27+
role_id = "8132" # all_repo_read (predefined)
28+
}
29+
```
30+
31+
## Argument Reference
32+
33+
The following arguments are supported:
34+
35+
* `team_slug` - (Required) The GitHub team slug
36+
* `role_id` - (Required) The GitHub organization role id
37+
38+
## Import
39+
40+
GitHub Team Organization Role Assignment can be imported using an ID made up of `team_slug:role_id`
41+
42+
```
43+
$ terraform import github_organization_role_team_assignment.role_assignment test-team:8132
44+
```

0 commit comments

Comments
 (0)