Skip to content

Commit

Permalink
feat: Updated repo collaborators to support ignoring teams
Browse files Browse the repository at this point in the history
Signed-off-by: Steve Hipwell <[email protected]>
  • Loading branch information
stevehipwell committed Nov 28, 2024
1 parent 1c11053 commit 1d433f2
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 29 deletions.
78 changes: 65 additions & 13 deletions github/resource_github_repository_collaborators.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func resourceGithubRepositoryCollaborators() *schema.Resource {
"user": {
Type: schema.TypeSet,
Optional: true,
Description: "List of users",
Description: "List of users.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"permission": {
Expand All @@ -52,7 +52,7 @@ func resourceGithubRepositoryCollaborators() *schema.Resource {
"team": {
Type: schema.TypeSet,
Optional: true,
Description: "List of teams",
Description: "List of teams.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"permission": {
Expand All @@ -76,6 +76,20 @@ func resourceGithubRepositoryCollaborators() *schema.Resource {
},
Computed: true,
},
"ignore_team": {
Type: schema.TypeSet,
Optional: true,
Description: "List of teams to ignore.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"team_id": {
Type: schema.TypeString,
Description: "ID or slug of the team to ignore.",
Required: true,
},
},
},
},
},

CustomizeDiff: customdiff.Sequence(
Expand Down Expand Up @@ -230,7 +244,8 @@ func listInvitations(client *github.Client, ctx context.Context, owner, repoName
permissionName := getPermission(i.GetPermissions())

invitedCollaborators = append(invitedCollaborators, invitedCollaborator{
userCollaborator{permissionName, i.GetInvitee().GetLogin()}, i.GetID()})
userCollaborator{permissionName, i.GetInvitee().GetLogin()}, i.GetID(),
})
}

if resp.NextPage == 0 {
Expand All @@ -241,7 +256,7 @@ func listInvitations(client *github.Client, ctx context.Context, owner, repoName
return invitedCollaborators, nil
}

func listTeams(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string) ([]teamCollaborator, error) {
func listTeams(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string, ignoreTeamIds []int64) ([]teamCollaborator, error) {
var teamCollaborators []teamCollaborator

if !isOrg {
Expand All @@ -256,6 +271,10 @@ func listTeams(client *github.Client, isOrg bool, ctx context.Context, owner, re
}

for _, t := range repoTeams {
if slices.Contains(ignoreTeamIds, t.GetID()) {
continue
}

permissionName := getPermission(t.GetPermission())

teamCollaborators = append(teamCollaborators, teamCollaborator{permissionName, t.GetID(), t.GetSlug()})
Expand All @@ -269,7 +288,7 @@ func listTeams(client *github.Client, isOrg bool, ctx context.Context, owner, re
return teamCollaborators, nil
}

func listAllCollaborators(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string) ([]userCollaborator, []invitedCollaborator, []teamCollaborator, error) {
func listAllCollaborators(client *github.Client, isOrg bool, ctx context.Context, owner, repoName string, ignoreTeamIds []int64) ([]userCollaborator, []invitedCollaborator, []teamCollaborator, error) {
userCollaborators, err := listUserCollaborators(client, isOrg, ctx, owner, repoName)
if err != nil {
return nil, nil, nil, err
Expand All @@ -278,7 +297,7 @@ func listAllCollaborators(client *github.Client, isOrg bool, ctx context.Context
if err != nil {
return nil, nil, nil, err
}
teamCollaborators, err := listTeams(client, isOrg, ctx, owner, repoName)
teamCollaborators, err := listTeams(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return nil, nil, nil, err
}
Expand All @@ -287,7 +306,8 @@ func listAllCollaborators(client *github.Client, isOrg bool, ctx context.Context

func matchUserCollaboratorsAndInvites(
repoName string, want []interface{}, hasUsers []userCollaborator, hasInvites []invitedCollaborator,
meta interface{}) error {
meta interface{},
) error {
client := meta.(*Owner).v3client

owner := meta.(*Owner).name
Expand Down Expand Up @@ -384,7 +404,8 @@ func matchUserCollaboratorsAndInvites(
}

func matchTeamCollaborators(
repoName string, want []interface{}, has []teamCollaborator, meta interface{}) error {
repoName string, want []interface{}, has []teamCollaborator, meta interface{},
) error {
client := meta.(*Owner).v3client
orgID := meta.(*Owner).id
owner := meta.(*Owner).name
Expand Down Expand Up @@ -471,15 +492,15 @@ func resourceGithubRepositoryCollaboratorsCreate(d *schema.ResourceData, meta in
repoName := d.Get("repository").(string)
ctx := context.Background()

teamsMap := make(map[string]struct{})
teamsMap := make(map[string]struct{}, len(teams))
for _, team := range teams {
teamIDString := team.(map[string]interface{})["team_id"].(string)
if _, found := teamsMap[teamIDString]; found {
return fmt.Errorf("duplicate set member: %s", teamIDString)
}
teamsMap[teamIDString] = struct{}{}
}
usersMap := make(map[string]struct{})
usersMap := make(map[string]struct{}, len(users))
for _, user := range users {
username := user.(map[string]interface{})["username"].(string)
if _, found := usersMap[username]; found {
Expand All @@ -488,7 +509,12 @@ func resourceGithubRepositoryCollaboratorsCreate(d *schema.ResourceData, meta in
usersMap[username] = struct{}{}
}

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName)
ignoreTeamIds, err := getIgnoreTeamIds(d, meta)
if err != nil {
return err
}

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "repository collaborators (%s/%s)", owner, repoName)
}
Expand Down Expand Up @@ -516,7 +542,12 @@ func resourceGithubRepositoryCollaboratorsRead(d *schema.ResourceData, meta inte
repoName := d.Id()
ctx := context.WithValue(context.Background(), ctxId, d.Id())

userCollaborators, invitedCollaborators, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName)
ignoreTeamIds, err := getIgnoreTeamIds(d, meta)
if err != nil {
return err
}

userCollaborators, invitedCollaborators, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "repository collaborators (%s/%s)", owner, repoName)
}
Expand Down Expand Up @@ -563,7 +594,12 @@ func resourceGithubRepositoryCollaboratorsDelete(d *schema.ResourceData, meta in
repoName := d.Get("repository").(string)
ctx := context.Background()

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName)
ignoreTeamIds, err := getIgnoreTeamIds(d, meta)
if err != nil {
return err
}

userCollaborators, invitations, teamCollaborators, err := listAllCollaborators(client, isOrg, ctx, owner, repoName, ignoreTeamIds)
if err != nil {
return deleteResourceOn404AndSwallow304OtherwiseReturnError(err, d, "repository collaborators (%s/%s)", owner, repoName)
}
Expand All @@ -580,3 +616,19 @@ func resourceGithubRepositoryCollaboratorsDelete(d *schema.ResourceData, meta in
err = matchTeamCollaborators(repoName, nil, teamCollaborators, meta)
return err
}

func getIgnoreTeamIds(d *schema.ResourceData, meta interface{}) ([]int64, error) {
ignoreTeams := d.Get("ignore_team").(*schema.Set).List()
ignoreTeamIds := make([]int64, len(ignoreTeams))

for i, t := range ignoreTeams {
s := t.(map[string]interface{})["team_id"].(string)
id, err := getTeamID(s, meta)
if err != nil {
return nil, err
}
ignoreTeamIds[i] = id
}

return ignoreTeamIds, nil
}
64 changes: 58 additions & 6 deletions github/resource_github_repository_collaborators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
)

func TestAccGithubRepositoryCollaborators(t *testing.T) {

inOrgUser := os.Getenv("GITHUB_IN_ORG_USER")
inOrgUser2 := os.Getenv("GITHUB_IN_ORG_USER2")

Expand All @@ -35,7 +34,6 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

t.Run("creates collaborators without error", func(t *testing.T) {

conn := meta.(*Owner).v3client
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)

Expand Down Expand Up @@ -192,7 +190,6 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
})

t.Run("updates collaborators without error", func(t *testing.T) {

conn := meta.(*Owner).v3client
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)

Expand Down Expand Up @@ -319,9 +316,7 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
t.Run("with an individual account", func(t *testing.T) {
check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("github_repository_collaborators.test_repo_collaborators", "user.#"),
resource.TestCheckResourceAttrSet("github_repository_collaborators.test_repo_collaborators", "team.#"),
resource.TestCheckResourceAttr("github_repository_collaborators.test_repo_collaborators", "user.#", "1"),
resource.TestCheckResourceAttr("github_repository_collaborators.test_repo_collaborators", "team.#", "0"),
func(state *terraform.State) error {
owner := meta.(*Owner).name

Expand Down Expand Up @@ -411,7 +406,6 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
})

t.Run("removes collaborators without error", func(t *testing.T) {

conn := meta.(*Owner).v3client
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)

Expand Down Expand Up @@ -544,4 +538,62 @@ func TestAccGithubRepositoryCollaborators(t *testing.T) {
testCase(t, organization, orgConfig, orgConfigUpdate, check)
})
})

t.Run("ignores specified teams", func(t *testing.T) {
repoName := fmt.Sprintf("tf-acc-test-%s", randomID)
team0Name := fmt.Sprintf("tf-acc-test-team0-%s", randomID)
team1Name := fmt.Sprintf("tf-acc-test-team1-%s", randomID)

config := fmt.Sprintf(`
resource "github_repository" "test" {
name = "%s"
auto_init = true
visibility = "private"
}
resource "github_team" "test_0" {
name = "%s"
}
resource "github_team_repository" "some_team_repo" {
team_id = github_team.test_0.id
repository = github_repository.test.name
}
resource "github_team" "test_1" {
name = "%s"
}
resource "github_repository_collaborators" "test_repo_collaborators" {
repository = "${github_repository.test.name}"
team {
team_id = github_team.test_1.id
permission = "pull"
}
ignore_team {
team_id = github_team.test_0.id
}
}
`, repoName, team0Name, team1Name)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, organization) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("github_repository_collaborators.test_repo_collaborators", "team.#"),
resource.TestCheckResourceAttr("github_repository_collaborators.test_repo_collaborators", "team.#", "1"),
),
},
{
Config: config,
ExpectNonEmptyPlan: false,
},
},
})
})
}
25 changes: 15 additions & 10 deletions website/docs/r/repository_collaborators.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ github_team_repository or they will fight over what your policy should be.

This resource allows you to manage all collaborators for repositories in your
organization or personal account. For organization repositories, collaborators can
have explicit (and differing levels of) read, write, or administrator access to
specific repositories, without giving the user full organization membership.
have explicit (and differing levels of) read, write, or administrator access to
specific repositories, without giving the user full organization membership.
For personal repositories, collaborators can only be granted write
(implicitly includes read) permission.
(implicitly includes read) permission.

When applied, an invitation will be sent to the user to become a collaborators
on a repository. When destroyed, either the invitation will be cancelled or the
Expand All @@ -31,7 +31,7 @@ Further documentation on GitHub collaborators:
- [Adding outside collaborators to your personal repositories](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/managing-access-to-your-personal-repositories)
- [Adding outside collaborators to repositories in your organization](https://help.github.com/articles/adding-outside-collaborators-to-repositories-in-your-organization/)
- [Converting an organization member to an outside collaborators](https://help.github.com/articles/converting-an-organization-member-to-an-outside-collaborator/)

## Example Usage

```hcl
Expand All @@ -52,7 +52,7 @@ resource "github_repository_collaborators" "some_repo_collaborators" {
permission = "admin"
username = "SomeUser"
}
team {
permission = "pull"
team_id = github_team.some_team.slug
Expand All @@ -64,9 +64,10 @@ resource "github_repository_collaborators" "some_repo_collaborators" {

The following arguments are supported:

* `repository` - (Required) The GitHub repository
* `user` - (Optional) List of users
* `team` - (Optional) List of teams
* `repository` - (Required) The GitHub repository.
* `user` - (Optional) List of users to grant access to the repository.
* `team` - (Optional) List of teams to grant access to the repository.
* `ignore_team` - (Optional) List of teams to ignore when checking for repository access. This supports ignoring teams granted access at an organizational level.

The `user` block supports:

Expand All @@ -77,16 +78,20 @@ The `user` block supports:

The `team` block supports:

* `team_id` - (Required) The GitHub team id or the GitHub team slug
* `team_id` - (Required) The GitHub team id or the GitHub team slug.
* `permission` - (Optional) The permission of the outside collaborators for the repository.
Must be one of `pull`, `triage`, `push`, `maintain`, `admin` or the name of an existing [custom repository role](https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-peoples-access-to-your-organization-with-roles/managing-custom-repository-roles-for-an-organization) within the organisation. Defaults to `pull`.
Must be `push` for personal repositories. Defaults to `push`.

The `team_ignore` block supports:

* `team_id` - (Required) The GitHub team id or the GitHub team slug.

## Attribute Reference

In addition to the above arguments, the following attributes are exported:

* `invitation_ids` - Map of usernames to invitation ID for any users added as part of creation of this resource to
* `invitation_ids` - Map of usernames to invitation ID for any users added as part of creation of this resource to
be used in [`github_user_invitation_accepter`](./user_invitation_accepter.html).

## Import
Expand Down

0 comments on commit 1d433f2

Please sign in to comment.