Skip to content
Open
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
1 change: 1 addition & 0 deletions RESOURCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ The overall status of each resource or data source is captured in this document
| `github_rest_api` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ |
| `github_ssh_keys` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ |
| `github_team` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `github_team_external_groups` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `github_team_members` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `github_team_repositories` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `github_tree` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ |
Expand Down
60 changes: 60 additions & 0 deletions docs/data-sources/team_external_groups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
page_title: "github_team_external_groups (Data Source) - GitHub"
subcategory: ""
description: |-
Data source to retrieve external groups for a specific GitHub team.
---

# github_team_external_groups (Data Source)

Data source to retrieve external groups for a specific GitHub team.

## Example Usage

```terraform
data "github_team_external_groups" "example" {
slug = "example"
}
```

<!--
## Schema

### Required

- `slug` (String) The slug of the GitHub team.

### Read-Only

- `external_groups` (List of Object) List of external groups connected to the team. (see [below for nested schema](#nestedatt--external_groups))
- `id` (String) The ID of this resource.

<a id="nestedatt--external_groups"></a>
### Nested Schema for `external_groups`

Read-Only:

- `group_id` (Number)
- `group_name` (String)
- `updated_at` (String)
-->

## Schema

### Required

- `slug` (String) The slug of the GitHub team.

### Read-Only

- `external_groups` (List of Object) List of external groups connected to the team. (see [below for nested schema](#nestedatt--external_groups))
- `id` (String) The ID of this resource.

<a id="nestedatt--external_groups"></a>
### Nested Schema for `external_groups`

Read-Only:

- `group_id` (Number) The internal ID of the group.
- `group_name` (String) The display name of the group.
- `updated_at` (String) The time of the last update for this group.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data "github_team_external_groups" "example" {
slug = "example"
}
28 changes: 28 additions & 0 deletions github/acc_helpers_team_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,31 @@ func mustAddRepositoryToTeam(t *testing.T, team *github.Team, repo *github.Repos
t.Fatalf("failed to add team %s to test repository %s: %v", team.GetName(), repo.GetName(), err)
}
}

func mustRemoveAllMembersFromTeam(t *testing.T, team *github.Team) {
t.Helper()

members, _, err := testAccConf.meta.v3client.Teams.ListTeamMembersBySlug(t.Context(), testAccConf.meta.name, team.GetSlug(), &github.TeamListTeamMembersOptions{})
if err != nil {
t.Fatalf("failed to list members of test team %s: %v", team.GetName(), err)
}

for _, member := range members {
if _, err := testAccConf.meta.v3client.Teams.RemoveTeamMembershipBySlug(t.Context(), testAccConf.meta.name, team.GetSlug(), member.GetLogin()); err != nil {
t.Fatalf("failed to remove member %s from test team %s: %v", member.GetLogin(), team.GetName(), err)
}
}
}

func mustConnectTeamToExternalGroup(t *testing.T, team *github.Team, externalGroupID int64) {
t.Helper()

mustRemoveAllMembersFromTeam(t, team)

_, _, err := testAccConf.meta.v3client.Teams.UpdateConnectedExternalGroup(t.Context(), testAccConf.meta.name, team.GetSlug(), github.UpdateConnectedExternalGroupRequest{
GroupID: externalGroupID,
})
if err != nil {
t.Fatalf("failed to connect team %s to external group %d: %v", team.GetName(), externalGroupID, err)
}
}
4 changes: 4 additions & 0 deletions github/acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ func TestMain(m *testing.M) {
testRepositoryVisibility: "public",
}

if i, err := strconv.Atoi(os.Getenv("GH_TEST_EXTERNAL_GROUP1_ID")); err == nil {
conf.testExternalGroup1ID = i
}

if conf.authMode != anonymous {
conf.owner = os.Getenv("GITHUB_OWNER")
conf.token = os.Getenv("GITHUB_TOKEN")
Expand Down
103 changes: 103 additions & 0 deletions github/data_source_github_team_external_groups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package github

import (
"context"
"errors"
"net/http"
"strings"
"time"

"github.com/google/go-github/v92/github"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

func dataSourceGithubTeamExternalGroups() *schema.Resource {
return &schema.Resource{
Description: "Data source to retrieve external groups for a specific GitHub team.",
ReadContext: dataSourceGithubTeamExternalGroupsRead,
Comment thread
orirawlings marked this conversation as resolved.
Schema: map[string]*schema.Schema{
Comment thread
orirawlings marked this conversation as resolved.
"slug": {
Type: schema.TypeString,
Required: true,
Description: "The slug of the GitHub team.",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty),
},
"external_groups": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add descriptions and a documentation template, see the team members implementation for how to do this.

Description: "List of external groups connected to the team.",
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"group_id": {
Description: "The internal ID of the group.",
Type: schema.TypeInt,
Computed: true,
},
"group_name": {
Description: "The display name of the group.",
Type: schema.TypeString,
Computed: true,
},
"updated_at": {
Description: "The time of the last update for this group.",
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
}
}

func dataSourceGithubTeamExternalGroupsRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta, _ := m.(*Owner)

if ok, diags := checkOrganizationOK(meta); !ok {
return diags
}

orgName := meta.name
slug, ok := d.Get("slug").(string)
if !ok {
return diag.Errorf("expected type of %s to be string", d.Get("slug"))
}

externalGroups, _, err := meta.v3client.Teams.ListExternalGroupsForTeamBySlug(ctx, orgName, slug)
if err != nil {
// Teams with explicit members cannot be externally managed, so they have no external groups.
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) && ghErr.Response != nil &&
ghErr.Response.StatusCode == http.StatusBadRequest &&
strings.Contains(ghErr.Message, "has explicit members") {
externalGroups = &github.ExternalGroupList{}
} else {
return diag.FromErr(err)
}
}

groups := make([]map[string]any, 0)
for _, group := range externalGroups.Groups {
g := map[string]any{
"group_id": group.GetGroupID(),
"group_name": group.GetGroupName(),
"updated_at": group.GetUpdatedAt().Format(time.RFC3339),
}

groups = append(groups, g)
}

id, err := buildID(orgName, slug)
if err != nil {
return diag.FromErr(err)
}
d.SetId(id)

if err := d.Set("external_groups", groups); err != nil {
return diag.Errorf("error setting external_groups: %v", err)
}

return nil
}
128 changes: 128 additions & 0 deletions github/data_source_github_team_external_groups_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package github

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

func TestAccGithubTeamExternalGroupsDataSource(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the happy path test?

t.Parallel()

skipUnlessEMUEnterprise(t)

t.Run("queries_empty_slug", func(t *testing.T) {
config := `
data "github_team_external_groups" "test" {
slug = ""
}
`

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile(`expected "slug" to not be an empty string`),
},
},
})
})

t.Run("queries_non_existent_team", func(t *testing.T) {
config := `
data "github_team_external_groups" "test" {
slug = "non-existing-team-slug"
}
`

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ExpectError: regexp.MustCompile(`Not Found`),
},
},
})
})

t.Run("queries_team_without_external_groups", func(t *testing.T) {
team := mustCreateTestTeam(t)
config := fmt.Sprintf(`
data "github_team_external_groups" "test" {
slug = "%s"
}
`, team.GetSlug())

mustRemoveAllMembersFromTeam(t, team)

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("data.github_team_external_groups.test", tfjsonpath.New("external_groups"), knownvalue.ListSizeExact(0)),
},
},
},
})
})

t.Run("queries_team_with_regular_membership", func(t *testing.T) {
team := mustCreateTestTeam(t)
config := fmt.Sprintf(`
data "github_team_external_groups" "test" {
slug = "%s"
}
`, team.GetSlug())

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("data.github_team_external_groups.test", tfjsonpath.New("external_groups"), knownvalue.ListSizeExact(0)),
},
},
},
})
})

t.Run("queries_team_with_external_groups", func(t *testing.T) {
if testAccConf.testExternalGroup1ID == 0 {
t.Skip("Skipping as no external groups are configured for the test organization")
}
team := mustCreateTestTeam(t)
groupID := int64(testAccConf.testExternalGroup1ID)
mustConnectTeamToExternalGroup(t, team, groupID)
config := fmt.Sprintf(`
data "github_team_external_groups" "test" {
slug = "%s"
}
`, team.GetSlug())

resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue("data.github_team_external_groups.test", tfjsonpath.New("external_groups"), knownvalue.ListExact([]knownvalue.Check{
knownvalue.MapPartial(map[string]knownvalue.Check{
"group_id": knownvalue.Int64Exact(groupID),
}),
})),
},
},
},
})
})
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ func NewProvider(version, commit string) func() *schema.Provider {
"github_team": dataSourceGithubTeam(),
"github_team_members": dataSourceGithubTeamMembers(),
"github_team_repositories": dataSourceGithubTeamRepositories(),
"github_team_external_groups": dataSourceGithubTeamExternalGroups(),
"github_tree": dataSourceGithubTree(),
"github_user": dataSourceGithubUser(),
"github_user_external_identity": dataSourceGithubUserExternalIdentity(),
Expand Down
Loading
Loading