From 4e58ce90f8bd2e292d1b004b9645f669f5d5ed90 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 21:02:35 +0100 Subject: [PATCH 1/2] feat: add enterprise teams resources and data sources Add support for managing GitHub Enterprise Teams (enterprise-level teams): Resources: - github_enterprise_team: create/update/delete enterprise teams - github_enterprise_team_membership: manage team members - github_enterprise_team_organizations: assign teams to organizations Data sources: - github_enterprise_team: lookup by slug or ID - github_enterprise_teams: list all enterprise teams - github_enterprise_team_membership: check user membership - github_enterprise_team_organizations: list assigned orgs Note: These endpoints require GitHub Enterprise Cloud with a classic PAT that has enterprise admin permissions. --- github/data_source_github_enterprise_team.go | 118 ++++++ ...ource_github_enterprise_team_membership.go | 83 ++++ ...ce_github_enterprise_team_organizations.go | 66 ++++ ...data_source_github_enterprise_team_test.go | 167 ++++++++ github/data_source_github_enterprise_teams.go | 106 ++++++ ...ata_source_github_enterprise_teams_test.go | 52 +++ github/provider.go | 7 + github/resource_github_enterprise_team.go | 279 ++++++++++++++ ...ource_github_enterprise_team_membership.go | 129 +++++++ ...ce_github_enterprise_team_organizations.go | 180 +++++++++ .../resource_github_enterprise_team_test.go | 211 +++++++++++ github/util_enterprise_teams.go | 357 ++++++++++++++++++ website/docs/d/enterprise_team.html.markdown | 49 +++ .../enterprise_team_membership.html.markdown | 38 ++ ...nterprise_team_organizations.html.markdown | 38 ++ website/docs/d/enterprise_teams.html.markdown | 45 +++ website/docs/r/enterprise_team.html.markdown | 53 +++ .../enterprise_team_membership.html.markdown | 47 +++ ...nterprise_team_organizations.html.markdown | 53 +++ 19 files changed, 2078 insertions(+) create mode 100644 github/data_source_github_enterprise_team.go create mode 100644 github/data_source_github_enterprise_team_membership.go create mode 100644 github/data_source_github_enterprise_team_organizations.go create mode 100644 github/data_source_github_enterprise_team_test.go create mode 100644 github/data_source_github_enterprise_teams.go create mode 100644 github/data_source_github_enterprise_teams_test.go create mode 100644 github/resource_github_enterprise_team.go create mode 100644 github/resource_github_enterprise_team_membership.go create mode 100644 github/resource_github_enterprise_team_organizations.go create mode 100644 github/resource_github_enterprise_team_test.go create mode 100644 github/util_enterprise_teams.go create mode 100644 website/docs/d/enterprise_team.html.markdown create mode 100644 website/docs/d/enterprise_team_membership.html.markdown create mode 100644 website/docs/d/enterprise_team_organizations.html.markdown create mode 100644 website/docs/d/enterprise_teams.html.markdown create mode 100644 website/docs/r/enterprise_team.html.markdown create mode 100644 website/docs/r/enterprise_team_membership.html.markdown create mode 100644 website/docs/r/enterprise_team_organizations.html.markdown diff --git a/github/data_source_github_enterprise_team.go b/github/data_source_github_enterprise_team.go new file mode 100644 index 0000000000..3eb0397e21 --- /dev/null +++ b/github/data_source_github_enterprise_team.go @@ -0,0 +1,118 @@ +package github + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubEnterpriseTeam() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGithubEnterpriseTeamRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + }, + "slug": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"team_id"}, + Description: "The slug of the enterprise team.", + }, + "team_id": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ConflictsWith: []string{"slug"}, + Description: "The numeric ID of the enterprise team.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the enterprise team.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "A description of the enterprise team.", + }, + "organization_selection_type": { + Type: schema.TypeString, + Computed: true, + Description: "Specifies which organizations in the enterprise should have access to this team.", + }, + "group_id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the IdP group to assign team membership with.", + }, + }, + } +} + +func dataSourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + if enterpriseSlug == "" { + return fmt.Errorf("enterprise_slug must not be empty") + } + + ctx := context.Background() + + var te *enterpriseTeam + if v, ok := d.GetOk("team_id"); ok { + teamID := int64(v.(int)) + if teamID != 0 { + found, err := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if err != nil { + return err + } + if found == nil { + return fmt.Errorf("could not find enterprise team %d in enterprise %s", teamID, enterpriseSlug) + } + te = found + } + } + + if te == nil { + teamSlug := strings.TrimSpace(d.Get("slug").(string)) + if teamSlug == "" { + return fmt.Errorf("one of slug or team_id must be set") + } + found, _, err := getEnterpriseTeamBySlug(ctx, client, enterpriseSlug, teamSlug) + if err != nil { + return err + } + te = found + } + + d.SetId(buildSlashTwoPartID(enterpriseSlug, strconv.FormatInt(te.ID, 10))) + _ = d.Set("enterprise_slug", enterpriseSlug) + _ = d.Set("slug", te.Slug) + _ = d.Set("team_id", int(te.ID)) + _ = d.Set("name", te.Name) + if te.Description != nil { + _ = d.Set("description", *te.Description) + } else { + _ = d.Set("description", "") + } + orgSel := te.OrganizationSelectionType + if orgSel == "" { + orgSel = "disabled" + } + _ = d.Set("organization_selection_type", orgSel) + if te.GroupID != nil { + _ = d.Set("group_id", *te.GroupID) + } else { + _ = d.Set("group_id", "") + } + + return nil +} diff --git a/github/data_source_github_enterprise_team_membership.go b/github/data_source_github_enterprise_team_membership.go new file mode 100644 index 0000000000..8323dac424 --- /dev/null +++ b/github/data_source_github_enterprise_team_membership.go @@ -0,0 +1,83 @@ +package github + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubEnterpriseTeamMembership() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGithubEnterpriseTeamMembershipRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + }, + "enterprise_team": { + Type: schema.TypeString, + Required: true, + Description: "The slug or ID of the enterprise team.", + }, + "username": { + Type: schema.TypeString, + Required: true, + Description: "The GitHub username.", + }, + "role": { + Type: schema.TypeString, + Computed: true, + Description: "The role of the user in the enterprise team, if returned by the API.", + }, + "state": { + Type: schema.TypeString, + Computed: true, + Description: "The membership state, if returned by the API.", + }, + "etag": { + Type: schema.TypeString, + Computed: true, + Description: "ETag of the membership response.", + }, + }, + } +} + +func dataSourceGithubEnterpriseTeamMembershipRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + enterpriseTeam := strings.TrimSpace(d.Get("enterprise_team").(string)) + username := strings.TrimSpace(d.Get("username").(string)) + if enterpriseSlug == "" { + return fmt.Errorf("enterprise_slug must not be empty") + } + if enterpriseTeam == "" { + return fmt.Errorf("enterprise_team must not be empty") + } + if username == "" { + return fmt.Errorf("username must not be empty") + } + + ctx := context.Background() + m, resp, err := getEnterpriseTeamMembershipDetails(ctx, client, enterpriseSlug, enterpriseTeam, username) + if err != nil { + return err + } + + d.SetId(buildSlashThreePartID(enterpriseSlug, enterpriseTeam, username)) + _ = d.Set("enterprise_slug", enterpriseSlug) + _ = d.Set("enterprise_team", enterpriseTeam) + _ = d.Set("username", username) + if m != nil { + _ = d.Set("role", m.Role) + _ = d.Set("state", m.State) + } + if resp != nil { + _ = d.Set("etag", resp.Header.Get("ETag")) + } + return nil +} diff --git a/github/data_source_github_enterprise_team_organizations.go b/github/data_source_github_enterprise_team_organizations.go new file mode 100644 index 0000000000..ae690980e7 --- /dev/null +++ b/github/data_source_github_enterprise_team_organizations.go @@ -0,0 +1,66 @@ +package github + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubEnterpriseTeamOrganizations() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGithubEnterpriseTeamOrganizationsRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + }, + "enterprise_team": { + Type: schema.TypeString, + Required: true, + Description: "The slug or ID of the enterprise team.", + }, + "organization_slugs": { + Type: schema.TypeSet, + Computed: true, + Description: "Set of organization slugs that the enterprise team is assigned to.", + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func dataSourceGithubEnterpriseTeamOrganizationsRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + enterpriseTeam := strings.TrimSpace(d.Get("enterprise_team").(string)) + if enterpriseSlug == "" { + return fmt.Errorf("enterprise_slug must not be empty") + } + if enterpriseTeam == "" { + return fmt.Errorf("enterprise_team must not be empty") + } + + ctx := context.Background() + orgs, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) + if err != nil { + return err + } + + slugs := make([]string, 0, len(orgs)) + for _, org := range orgs { + if org.Login != "" { + slugs = append(slugs, org.Login) + } + } + + d.SetId(buildSlashTwoPartID(enterpriseSlug, enterpriseTeam)) + _ = d.Set("enterprise_slug", enterpriseSlug) + _ = d.Set("enterprise_team", enterpriseTeam) + _ = d.Set("organization_slugs", slugs) + return nil +} diff --git a/github/data_source_github_enterprise_team_test.go b/github/data_source_github_enterprise_team_test.go new file mode 100644 index 0000000000..6cd5cac2d1 --- /dev/null +++ b/github/data_source_github_enterprise_team_test.go @@ -0,0 +1,167 @@ +package github + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubEnterpriseTeamDataSource(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-ds-team-%s" + } + + data "github_enterprise_team" "by_slug" { + enterprise_slug = data.github_enterprise.enterprise.slug + slug = github_enterprise_team.test.slug + } + + data "github_enterprise_team" "by_id" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_id = github_enterprise_team.test.team_id + } + `, testEnterprise, randomID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.github_enterprise_team.by_slug", "id"), + resource.TestCheckResourceAttrPair("data.github_enterprise_team.by_slug", "team_id", "github_enterprise_team.test", "team_id"), + resource.TestCheckResourceAttrPair("data.github_enterprise_team.by_slug", "slug", "github_enterprise_team.test", "slug"), + resource.TestCheckResourceAttrPair("data.github_enterprise_team.by_slug", "name", "github_enterprise_team.test", "name"), + resource.TestCheckResourceAttrSet("data.github_enterprise_team.by_id", "id"), + resource.TestCheckResourceAttrPair("data.github_enterprise_team.by_id", "team_id", "github_enterprise_team.test", "team_id"), + resource.TestCheckResourceAttrPair("data.github_enterprise_team.by_id", "slug", "github_enterprise_team.test", "slug"), + ), + }, + }, + }) +} + +func TestAccGithubEnterpriseTeamOrganizationsDataSource(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + if testOrganization == "" { + t.Skip("Skipping because `GITHUB_OWNER`/`GITHUB_ORGANIZATION` is not set") + } + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-ds-team-orgs-%s" + organization_selection_type = "selected" + } + + resource "github_enterprise_team_organizations" "assign" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.test.slug + organization_slugs = ["%s"] + } + + data "github_enterprise_team_organizations" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.test.slug + depends_on = [github_enterprise_team_organizations.assign] + } + `, testEnterprise, randomID, testOrganization) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.github_enterprise_team_organizations.test", "id"), + resource.TestCheckResourceAttr("data.github_enterprise_team_organizations.test", "organization_slugs.#", "1"), + resource.TestCheckTypeSetElemAttr("data.github_enterprise_team_organizations.test", "organization_slugs.*", testOrganization), + ), + }, + }, + }) +} + +func TestAccGithubEnterpriseTeamMembershipDataSource(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + username := os.Getenv("GITHUB_TEST_USER") + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + if username == "" { + t.Skip("Skipping because `GITHUB_TEST_USER` is not set") + } + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-ds-team-member-%s" + } + + resource "github_enterprise_team_membership" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.test.slug + username = "%s" + } + + data "github_enterprise_team_membership" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.test.slug + username = "%s" + depends_on = [github_enterprise_team_membership.test] + } + `, testEnterprise, randomID, username, username) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.github_enterprise_team_membership.test", "id"), + resource.TestCheckResourceAttr("data.github_enterprise_team_membership.test", "username", username), + ), + }, + }, + }) +} diff --git a/github/data_source_github_enterprise_teams.go b/github/data_source_github_enterprise_teams.go new file mode 100644 index 0000000000..1f774f0dee --- /dev/null +++ b/github/data_source_github_enterprise_teams.go @@ -0,0 +1,106 @@ +package github + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubEnterpriseTeams() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGithubEnterpriseTeamsRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + }, + "teams": { + Type: schema.TypeList, + Computed: true, + Description: "All teams in the enterprise.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "team_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The numeric ID of the enterprise team.", + }, + "slug": { + Type: schema.TypeString, + Computed: true, + Description: "The slug of the enterprise team.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the enterprise team.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "A description of the enterprise team.", + }, + "organization_selection_type": { + Type: schema.TypeString, + Computed: true, + Description: "Specifies which organizations in the enterprise should have access to this team.", + }, + "group_id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the IdP group to assign team membership with.", + }, + }, + }, + }, + }, + } +} + +func dataSourceGithubEnterpriseTeamsRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + if enterpriseSlug == "" { + return fmt.Errorf("enterprise_slug must not be empty") + } + + ctx := context.Background() + teams, err := listEnterpriseTeams(ctx, client, enterpriseSlug) + if err != nil { + return err + } + + flat := make([]any, 0, len(teams)) + for _, t := range teams { + m := map[string]any{ + "team_id": int(t.ID), + "slug": t.Slug, + "name": t.Name, + } + if t.Description != nil { + m["description"] = *t.Description + } else { + m["description"] = "" + } + orgSel := t.OrganizationSelectionType + if orgSel == "" { + orgSel = "disabled" + } + m["organization_selection_type"] = orgSel + if t.GroupID != nil { + m["group_id"] = *t.GroupID + } else { + m["group_id"] = "" + } + flat = append(flat, m) + } + + d.SetId(enterpriseSlug) + _ = d.Set("enterprise_slug", enterpriseSlug) + _ = d.Set("teams", flat) + return nil +} diff --git a/github/data_source_github_enterprise_teams_test.go b/github/data_source_github_enterprise_teams_test.go new file mode 100644 index 0000000000..6975fbf519 --- /dev/null +++ b/github/data_source_github_enterprise_teams_test.go @@ -0,0 +1,52 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubEnterpriseTeamsDataSource(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-ds-enterprise-teams-%s" + } + + data "github_enterprise_teams" "all" { + enterprise_slug = data.github_enterprise.enterprise.slug + depends_on = [github_enterprise_team.test] + } + `, testEnterprise, randomID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.github_enterprise_teams.all", "id"), + resource.TestCheckResourceAttrSet("data.github_enterprise_teams.all", "teams.0.team_id"), + resource.TestCheckResourceAttrSet("data.github_enterprise_teams.all", "teams.0.slug"), + resource.TestCheckResourceAttrSet("data.github_enterprise_teams.all", "teams.0.name"), + ), + }, + }, + }) +} diff --git a/github/provider.go b/github/provider.go index a10d84c027..31eadb587f 100644 --- a/github/provider.go +++ b/github/provider.go @@ -208,6 +208,9 @@ func Provider() *schema.Provider { "github_user_invitation_accepter": resourceGithubUserInvitationAccepter(), "github_user_ssh_key": resourceGithubUserSshKey(), "github_enterprise_organization": resourceGithubEnterpriseOrganization(), + "github_enterprise_team": resourceGithubEnterpriseTeam(), + "github_enterprise_team_membership": resourceGithubEnterpriseTeamMembership(), + "github_enterprise_team_organizations": resourceGithubEnterpriseTeamOrganizations(), "github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(), "github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(), "github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(), @@ -286,6 +289,10 @@ func Provider() *schema.Provider { "github_user_external_identity": dataSourceGithubUserExternalIdentity(), "github_users": dataSourceGithubUsers(), "github_enterprise": dataSourceGithubEnterprise(), + "github_enterprise_team": dataSourceGithubEnterpriseTeam(), + "github_enterprise_teams": dataSourceGithubEnterpriseTeams(), + "github_enterprise_team_membership": dataSourceGithubEnterpriseTeamMembership(), + "github_enterprise_team_organizations": dataSourceGithubEnterpriseTeamOrganizations(), "github_repository_environment_deployment_policies": dataSourceGithubRepositoryEnvironmentDeploymentPolicies(), }, } diff --git a/github/resource_github_enterprise_team.go b/github/resource_github_enterprise_team.go new file mode 100644 index 0000000000..11d19d01c9 --- /dev/null +++ b/github/resource_github_enterprise_team.go @@ -0,0 +1,279 @@ +package github + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "strconv" + "strings" + + githubv3 "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubEnterpriseTeam() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubEnterpriseTeamCreate, + Read: resourceGithubEnterpriseTeamRead, + Update: resourceGithubEnterpriseTeamUpdate, + Delete: resourceGithubEnterpriseTeamDelete, + Importer: &schema.ResourceImporter{State: resourceGithubEnterpriseTeamImport}, + + CustomizeDiff: customdiff.Sequence( + customdiff.ComputedIf("slug", func(_ context.Context, d *schema.ResourceDiff, meta any) bool { + return d.HasChange("name") + }), + ), + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise (e.g. from the enterprise URL).", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the enterprise team.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "A description of the enterprise team.", + }, + "organization_selection_type": { + Type: schema.TypeString, + Optional: true, + Default: "disabled", + Description: "Controls which organizations can see this team: `disabled`, `selected`, or `all`.", + ValidateDiagFunc: toDiagFunc( + validation.StringInSlice([]string{"disabled", "selected", "all"}, false), + "organization_selection_type", + ), + }, + "group_id": { + Type: schema.TypeString, + Optional: true, + Description: "The ID of the IdP group to assign team membership with.", + }, + "slug": { + Type: schema.TypeString, + Computed: true, + Description: "The slug of the enterprise team. GitHub generates the slug from the team name and adds the ent: prefix.", + }, + "team_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The numeric ID of the enterprise team.", + }, + }, + } +} + +func resourceGithubEnterpriseTeamCreate(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + + name := d.Get("name").(string) + description := d.Get("description").(string) + orgSelection := d.Get("organization_selection_type").(string) + groupID := d.Get("group_id").(string) + + req := enterpriseTeamCreateRequest{ + Name: name, + Description: githubv3.String(description), + OrganizationSelectionType: githubv3.String(orgSelection), + } + if groupID != "" { + req.GroupID = githubv3.String(groupID) + } + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + te, _, err := createEnterpriseTeam(ctx, client, enterpriseSlug, req) + if err != nil { + return err + } + + d.SetId(strconv.FormatInt(te.ID, 10)) + return resourceGithubEnterpriseTeamRead(d, meta) +} + +func resourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + + teamID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + // Try to fetch by slug first (faster), but if the team was renamed we need + // to fall back to listing all teams and matching by numeric ID. + var te *enterpriseTeam + if slug, ok := d.GetOk("slug"); ok { + if s := strings.TrimSpace(slug.(string)); s != "" { + candidate, _, getErr := getEnterpriseTeamBySlug(ctx, client, enterpriseSlug, s) + if getErr == nil { + te = candidate + } else { + ghErr := &githubv3.ErrorResponse{} + if errors.As(getErr, &ghErr) && ghErr.Response.StatusCode != http.StatusNotFound { + return getErr + } + } + } + } + + if te == nil { + te, err = findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if err != nil { + return err + } + if te == nil { + log.Printf("[INFO] Removing enterprise team %s/%s from state because it no longer exists in GitHub", enterpriseSlug, d.Id()) + d.SetId("") + return nil + } + } + + if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { + return err + } + if err = d.Set("name", te.Name); err != nil { + return err + } + if te.Description != nil { + if err = d.Set("description", *te.Description); err != nil { + return err + } + } else { + if err = d.Set("description", ""); err != nil { + return err + } + } + if err = d.Set("slug", te.Slug); err != nil { + return err + } + if err = d.Set("team_id", int(te.ID)); err != nil { + return err + } + orgSelection := te.OrganizationSelectionType + if orgSelection == "" { + orgSelection = "disabled" + } + if err = d.Set("organization_selection_type", orgSelection); err != nil { + return err + } + if te.GroupID != nil { + if err = d.Set("group_id", *te.GroupID); err != nil { + return err + } + } else { + if err = d.Set("group_id", ""); err != nil { + return err + } + } + + return nil +} + +func resourceGithubEnterpriseTeamUpdate(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + + // We need a team slug for the API. If state is missing, re-discover it by ID. + teamSlug := strings.TrimSpace(d.Get("slug").(string)) + if teamSlug == "" { + teamID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + te, err := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if err != nil { + return err + } + if te == nil { + return fmt.Errorf("enterprise team %s no longer exists", d.Id()) + } + teamSlug = te.Slug + } + + name := d.Get("name").(string) + description := d.Get("description").(string) + orgSelection := d.Get("organization_selection_type").(string) + groupID := d.Get("group_id").(string) + + req := enterpriseTeamUpdateRequest{ + Name: githubv3.String(name), + Description: githubv3.String(description), + OrganizationSelectionType: githubv3.String(orgSelection), + } + if groupID != "" { + req.GroupID = githubv3.String(groupID) + } + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + _, _, err := updateEnterpriseTeam(ctx, client, enterpriseSlug, teamSlug, req) + if err != nil { + return err + } + + return resourceGithubEnterpriseTeamRead(d, meta) +} + +func resourceGithubEnterpriseTeamDelete(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + teamSlug := strings.TrimSpace(d.Get("slug").(string)) + if teamSlug == "" { + teamID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + te, err := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if err != nil { + return err + } + if te == nil { + return nil + } + teamSlug = te.Slug + } + + log.Printf("[INFO] Deleting enterprise team: %s/%s (%s)", enterpriseSlug, teamSlug, d.Id()) + resp, err := deleteEnterpriseTeam(ctx, client, enterpriseSlug, teamSlug) + if err != nil { + // Already gone? That's fine, we wanted it deleted anyway. + ghErr := &githubv3.ErrorResponse{} + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + _ = resp + return err + } + + return nil +} + +func resourceGithubEnterpriseTeamImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + // Import format: / + parts := strings.Split(d.Id(), "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid import specified: supplied import must be written as /") + } + + enterpriseSlug, teamID := parts[0], parts[1] + d.SetId(teamID) + _ = d.Set("enterprise_slug", enterpriseSlug) + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_enterprise_team_membership.go b/github/resource_github_enterprise_team_membership.go new file mode 100644 index 0000000000..1719ca232b --- /dev/null +++ b/github/resource_github_enterprise_team_membership.go @@ -0,0 +1,129 @@ +package github + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + + githubv3 "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubEnterpriseTeamMembership() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubEnterpriseTeamMembershipCreate, + Read: resourceGithubEnterpriseTeamMembershipRead, + Delete: resourceGithubEnterpriseTeamMembershipDelete, + Importer: &schema.ResourceImporter{State: resourceGithubEnterpriseTeamMembershipImport}, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise.", + }, + "enterprise_team": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug or ID of the enterprise team.", + }, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: caseInsensitive(), + Description: "The login handle of the user.", + }, + }, + } +} + +func resourceGithubEnterpriseTeamMembershipCreate(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + enterpriseTeam := d.Get("enterprise_team").(string) + username := d.Get("username").(string) + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + // The API is idempotent, so we don't need to check if they're already a member + _, err := addEnterpriseTeamMember(ctx, client, enterpriseSlug, enterpriseTeam, username) + if err != nil { + return err + } + + // NOTE: enterprise team slugs have the "ent:" prefix, so we must not use + // colon-delimited IDs here. + d.SetId(buildSlashThreePartID(enterpriseSlug, enterpriseTeam, username)) + return resourceGithubEnterpriseTeamMembershipRead(d, meta) +} + +func resourceGithubEnterpriseTeamMembershipRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug, enterpriseTeam, username, err := parseSlashThreePartID(d.Id(), "enterprise_slug", "enterprise_team", "username") + if err != nil { + return err + } + + if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { + return err + } + if err = d.Set("enterprise_team", enterpriseTeam); err != nil { + return err + } + if err = d.Set("username", username); err != nil { + return err + } + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + _, err = getEnterpriseTeamMembership(ctx, client, enterpriseSlug, enterpriseTeam, username) + if err != nil { + ghErr := &githubv3.ErrorResponse{} + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode == http.StatusNotFound { + log.Printf("[INFO] Removing enterprise team membership %s from state because it no longer exists in GitHub", d.Id()) + d.SetId("") + return nil + } + } + return err + } + + return nil +} + +func resourceGithubEnterpriseTeamMembershipDelete(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + enterpriseTeam := d.Get("enterprise_team").(string) + username := d.Get("username").(string) + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + resp, err := removeEnterpriseTeamMember(ctx, client, enterpriseSlug, enterpriseTeam, username) + if err != nil { + ghErr := &githubv3.ErrorResponse{} + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + _ = resp + return err + } + + return nil +} + +func resourceGithubEnterpriseTeamMembershipImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + enterpriseSlug, enterpriseTeam, username, err := parseSlashThreePartID(d.Id(), "enterprise_slug", "enterprise_team", "username") + if err != nil { + return nil, fmt.Errorf("invalid import specified: supplied import must be written as //") + } + d.SetId(buildSlashThreePartID(enterpriseSlug, enterpriseTeam, username)) + _ = d.Set("enterprise_slug", enterpriseSlug) + _ = d.Set("enterprise_team", enterpriseTeam) + _ = d.Set("username", username) + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_enterprise_team_organizations.go b/github/resource_github_enterprise_team_organizations.go new file mode 100644 index 0000000000..bb0c739ec2 --- /dev/null +++ b/github/resource_github_enterprise_team_organizations.go @@ -0,0 +1,180 @@ +package github + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "strings" + + githubv3 "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubEnterpriseTeamOrganizations() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate, + Read: resourceGithubEnterpriseTeamOrganizationsRead, + Update: resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate, + Delete: resourceGithubEnterpriseTeamOrganizationsDelete, + Importer: &schema.ResourceImporter{State: resourceGithubEnterpriseTeamOrganizationsImport}, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise.", + }, + "enterprise_team": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug or ID of the enterprise team.", + }, + "organization_slugs": { + Type: schema.TypeSet, + Optional: true, + Description: "Set of organization slugs that the enterprise team should be assigned to.", + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + enterpriseTeam := d.Get("enterprise_team").(string) + + desiredSet := map[string]struct{}{} + if v, ok := d.GetOk("organization_slugs"); ok { + for _, s := range v.(*schema.Set).List() { + slug := strings.TrimSpace(s.(string)) + if slug != "" { + desiredSet[slug] = struct{}{} + } + } + } + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + current, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) + if err != nil { + return err + } + + currentSet := map[string]struct{}{} + for _, org := range current { + if org.Login != "" { + currentSet[org.Login] = struct{}{} + } + } + + toAdd := []string{} + for slug := range desiredSet { + if _, ok := currentSet[slug]; !ok { + toAdd = append(toAdd, slug) + } + } + + toRemove := []string{} + for slug := range currentSet { + if _, ok := desiredSet[slug]; !ok { + toRemove = append(toRemove, slug) + } + } + + // Perform adds before removes to avoid transient states where the team has no orgs + if err := addEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam, toAdd); err != nil { + return err + } + if _, err := removeEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam, toRemove); err != nil { + return err + } + + // NOTE: enterprise team slugs have the "ent:" prefix, so we must not use + // colon-delimited IDs here. + d.SetId(buildSlashTwoPartID(enterpriseSlug, enterpriseTeam)) + return resourceGithubEnterpriseTeamOrganizationsRead(d, meta) +} + +func resourceGithubEnterpriseTeamOrganizationsRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug, enterpriseTeam, err := parseSlashTwoPartID(d.Id(), "enterprise_slug", "enterprise_team") + if err != nil { + return err + } + + if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { + return err + } + if err = d.Set("enterprise_team", enterpriseTeam); err != nil { + return err + } + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + orgs, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) + if err != nil { + ghErr := &githubv3.ErrorResponse{} + if errors.As(err, &ghErr) { + if ghErr.Response.StatusCode == http.StatusNotFound { + log.Printf("[INFO] Removing enterprise team organizations %s from state because it no longer exists in GitHub", d.Id()) + d.SetId("") + return nil + } + } + return err + } + + slugs := []string{} + for _, org := range orgs { + if org.Login != "" { + slugs = append(slugs, org.Login) + } + } + if err = d.Set("organization_slugs", slugs); err != nil { + return err + } + + return nil +} + +func resourceGithubEnterpriseTeamOrganizationsDelete(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + enterpriseTeam := d.Get("enterprise_team").(string) + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + orgs, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) + if err != nil { + ghErr := &githubv3.ErrorResponse{} + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + return err + } + + toRemove := []string{} + for _, org := range orgs { + if org.Login != "" { + toRemove = append(toRemove, org.Login) + } + } + + log.Printf("[INFO] Removing all organization assignments for enterprise team: %s/%s", enterpriseSlug, enterpriseTeam) + _, err = removeEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam, toRemove) + return err +} + +func resourceGithubEnterpriseTeamOrganizationsImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + enterpriseSlug, enterpriseTeam, err := parseSlashTwoPartID(d.Id(), "enterprise_slug", "enterprise_team") + if err != nil { + return nil, fmt.Errorf("invalid import specified: supplied import must be written as /") + } + d.SetId(buildSlashTwoPartID(enterpriseSlug, enterpriseTeam)) + _ = d.Set("enterprise_slug", enterpriseSlug) + _ = d.Set("enterprise_team", enterpriseTeam) + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_enterprise_team_test.go b/github/resource_github_enterprise_team_test.go new file mode 100644 index 0000000000..2793d27361 --- /dev/null +++ b/github/resource_github_enterprise_team_test.go @@ -0,0 +1,211 @@ +package github + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubEnterpriseTeam(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + + config1 := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-team-%s" + description = "team for acceptance testing" + organization_selection_type = "disabled" + } + `, testEnterprise, randomID) + + config2 := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-team-%s" + description = "updated description" + organization_selection_type = "selected" + } + `, testEnterprise, randomID) + + check1 := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("github_enterprise_team.test", "slug"), + resource.TestCheckResourceAttrSet("github_enterprise_team.test", "team_id"), + resource.TestCheckResourceAttr("github_enterprise_team.test", "organization_selection_type", "disabled"), + ) + check2 := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_enterprise_team.test", "description", "updated description"), + resource.TestCheckResourceAttr("github_enterprise_team.test", "organization_selection_type", "selected"), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + {Config: config1, Check: check1}, + {Config: config2, Check: check2}, + { + ResourceName: "github_enterprise_team.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdPrefix: fmt.Sprintf(`%s/`, testEnterprise), + }, + }, + }) + } + + t.Run("with an enterprise account", func(t *testing.T) { + testCase(t, enterprise) + }) +} + +func TestAccGithubEnterpriseTeamOrganizations(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + if testOrganization == "" { + t.Skip("Skipping because `GITHUB_OWNER`/`GITHUB_ORGANIZATION` is not set") + } + + config1 := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-team-orgs-%s" + organization_selection_type = "selected" + } + + resource "github_enterprise_team_organizations" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.test.slug + organization_slugs = ["%s"] + } + `, testEnterprise, randomID, testOrganization) + + config2 := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-team-orgs-%s" + organization_selection_type = "selected" + } + + resource "github_enterprise_team_organizations" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.test.slug + organization_slugs = [] + } + `, testEnterprise, randomID) + + check1 := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_enterprise_team_organizations.test", "organization_slugs.#", "1"), + resource.TestCheckTypeSetElemAttr("github_enterprise_team_organizations.test", "organization_slugs.*", testOrganization), + ) + check2 := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_enterprise_team_organizations.test", "organization_slugs.#", "0"), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + {Config: config1, Check: check1}, + {Config: config2, Check: check2}, + { + ResourceName: "github_enterprise_team_organizations.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + } + + t.Run("with an enterprise account", func(t *testing.T) { + testCase(t, enterprise) + }) +} + +func TestAccGithubEnterpriseTeamMembership(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + username := os.Getenv("GITHUB_TEST_USER") + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + if username == "" { + t.Skip("Skipping because `GITHUB_TEST_USER` is not set") + } + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-team-member-%s" + } + + resource "github_enterprise_team_membership" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.test.slug + username = "%s" + } + `, testEnterprise, randomID, username) + + check := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("github_enterprise_team_membership.test", "username", username), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + {Config: config, Check: check}, + { + ResourceName: "github_enterprise_team_membership.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + } + + t.Run("with an enterprise account", func(t *testing.T) { + testCase(t, enterprise) + }) +} diff --git a/github/util_enterprise_teams.go b/github/util_enterprise_teams.go new file mode 100644 index 0000000000..f972004e3a --- /dev/null +++ b/github/util_enterprise_teams.go @@ -0,0 +1,357 @@ +package github + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + githubv3 "github.com/google/go-github/v67/github" +) + +const enterpriseTeamsAPIVersion = "2022-11-28" + +func parseSlashTwoPartID(id, left, right string) (string, string, error) { + parts := strings.SplitN(id, "/", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("unexpected ID format (%q); expected %s/%s", id, left, right) + } + return parts[0], parts[1], nil +} + +func buildSlashTwoPartID(a, b string) string { + return fmt.Sprintf("%s/%s", a, b) +} + +func parseSlashThreePartID(id, left, center, right string) (string, string, string, error) { + parts := strings.SplitN(id, "/", 3) + if len(parts) != 3 { + return "", "", "", fmt.Errorf("unexpected ID format (%q); expected %s/%s/%s", id, left, center, right) + } + return parts[0], parts[1], parts[2], nil +} + +func buildSlashThreePartID(a, b, c string) string { + return fmt.Sprintf("%s/%s/%s", a, b, c) +} + +func enterpriseTeamsAddListOptions(u string, opt *githubv3.ListOptions) string { + if opt == nil { + return u + } + vals := url.Values{} + if opt.Page != 0 { + vals.Set("page", strconv.Itoa(opt.Page)) + } + if opt.PerPage != 0 { + vals.Set("per_page", strconv.Itoa(opt.PerPage)) + } + enc := vals.Encode() + if enc == "" { + return u + } + if strings.Contains(u, "?") { + return u + "&" + enc + } + return u + "?" + enc +} + +func enterpriseTeamsNewRequest(client *githubv3.Client, method, urlStr string, body any) (*http.Request, error) { + req, err := client.NewRequest(method, urlStr, body) + if err != nil { + return nil, err + } + + // These endpoints are versioned and currently in public preview. + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", enterpriseTeamsAPIVersion) + return req, nil +} + +type enterpriseTeam struct { + ID int64 `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Slug string `json:"slug"` + GroupID *string `json:"group_id"` + OrganizationSelectionType string `json:"organization_selection_type"` +} + +type enterpriseTeamCreateRequest struct { + Name string `json:"name"` + Description *string `json:"description,omitempty"` + OrganizationSelectionType *string `json:"organization_selection_type,omitempty"` + GroupID *string `json:"group_id,omitempty"` +} + +type enterpriseTeamUpdateRequest struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + OrganizationSelectionType *string `json:"organization_selection_type,omitempty"` + GroupID *string `json:"group_id,omitempty"` +} + +func parseEnterpriseTeam(raw json.RawMessage) (*enterpriseTeam, error) { + // The API docs are inconsistent about whether this returns an object or an + // array with one element, so we try both. + var t enterpriseTeam + if err := json.Unmarshal(raw, &t); err == nil { + if t.ID != 0 || t.Slug != "" || t.Name != "" { + return &t, nil + } + } + + var ts []enterpriseTeam + if err := json.Unmarshal(raw, &ts); err == nil { + if len(ts) > 0 { + return &ts[0], nil + } + } + + return nil, fmt.Errorf("unexpected enterprise team response") +} + +func listEnterpriseTeams(ctx context.Context, client *githubv3.Client, enterpriseSlug string) ([]enterpriseTeam, error) { + all := []enterpriseTeam{} + opt := &githubv3.ListOptions{PerPage: maxPerPage} + + for { + u := enterpriseTeamsAddListOptions(fmt.Sprintf("enterprises/%s/teams", enterpriseSlug), opt) + req, err := enterpriseTeamsNewRequest(client, "GET", u, nil) + if err != nil { + return nil, err + } + + var pageTeams []enterpriseTeam + resp, err := client.Do(ctx, req, &pageTeams) + if err != nil { + return nil, err + } + all = append(all, pageTeams...) + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return all, nil +} + +func getEnterpriseTeamBySlug(ctx context.Context, client *githubv3.Client, enterpriseSlug, teamSlug string) (*enterpriseTeam, *githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams/%s", enterpriseSlug, teamSlug) + req, err := enterpriseTeamsNewRequest(client, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var raw json.RawMessage + resp, err := client.Do(ctx, req, &raw) + if err != nil { + return nil, resp, err + } + + te, err := parseEnterpriseTeam(raw) + return te, resp, err +} + +func createEnterpriseTeam(ctx context.Context, client *githubv3.Client, enterpriseSlug string, reqBody enterpriseTeamCreateRequest) (*enterpriseTeam, *githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams", enterpriseSlug) + req, err := enterpriseTeamsNewRequest(client, "POST", u, reqBody) + if err != nil { + return nil, nil, err + } + + var raw json.RawMessage + resp, err := client.Do(ctx, req, &raw) + if err != nil { + return nil, resp, err + } + + te, err := parseEnterpriseTeam(raw) + return te, resp, err +} + +func updateEnterpriseTeam(ctx context.Context, client *githubv3.Client, enterpriseSlug, teamSlug string, reqBody enterpriseTeamUpdateRequest) (*enterpriseTeam, *githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams/%s", enterpriseSlug, teamSlug) + req, err := enterpriseTeamsNewRequest(client, "PATCH", u, reqBody) + if err != nil { + return nil, nil, err + } + + var raw json.RawMessage + resp, err := client.Do(ctx, req, &raw) + if err != nil { + return nil, resp, err + } + + te, err := parseEnterpriseTeam(raw) + return te, resp, err +} + +func deleteEnterpriseTeam(ctx context.Context, client *githubv3.Client, enterpriseSlug, teamSlug string) (*githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams/%s", enterpriseSlug, teamSlug) + req, err := enterpriseTeamsNewRequest(client, "DELETE", u, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + return resp, nil +} + +func findEnterpriseTeamByID(ctx context.Context, client *githubv3.Client, enterpriseSlug string, id int64) (*enterpriseTeam, error) { + teams, err := listEnterpriseTeams(ctx, client, enterpriseSlug) + if err != nil { + return nil, err + } + for _, t := range teams { + if t.ID == id { + copy := t + return ©, nil + } + } + return nil, nil +} + +type enterpriseOrg struct { + Login string `json:"login"` + ID int64 `json:"id"` +} + +func listEnterpriseTeamOrganizations(ctx context.Context, client *githubv3.Client, enterpriseSlug, enterpriseTeam string) ([]enterpriseOrg, error) { + all := []enterpriseOrg{} + opt := &githubv3.ListOptions{PerPage: maxPerPage} + + for { + u := enterpriseTeamsAddListOptions(fmt.Sprintf("enterprises/%s/teams/%s/organizations", enterpriseSlug, enterpriseTeam), opt) + req, err := enterpriseTeamsNewRequest(client, "GET", u, nil) + if err != nil { + return nil, err + } + + var pageOrgs []enterpriseOrg + resp, err := client.Do(ctx, req, &pageOrgs) + if err != nil { + // Some docs show a single object; tolerate that. + var ghErr *githubv3.ErrorResponse + if errors.As(err, &ghErr) { + return nil, err + } + return nil, err + } + all = append(all, pageOrgs...) + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + return all, nil +} + +type enterpriseTeamOrgSlugsRequest struct { + OrganizationSlugs []string `json:"organization_slugs"` +} + +func addEnterpriseTeamOrganizations(ctx context.Context, client *githubv3.Client, enterpriseSlug, enterpriseTeam string, orgSlugs []string) error { + if len(orgSlugs) == 0 { + return nil + } + u := fmt.Sprintf("enterprises/%s/teams/%s/organizations/add", enterpriseSlug, enterpriseTeam) + req, err := enterpriseTeamsNewRequest(client, "POST", u, enterpriseTeamOrgSlugsRequest{OrganizationSlugs: orgSlugs}) + if err != nil { + return err + } + _, err = client.Do(ctx, req, nil) + return err +} + +func removeEnterpriseTeamOrganizations(ctx context.Context, client *githubv3.Client, enterpriseSlug, enterpriseTeam string, orgSlugs []string) (*githubv3.Response, error) { + if len(orgSlugs) == 0 { + return nil, nil + } + u := fmt.Sprintf("enterprises/%s/teams/%s/organizations/remove", enterpriseSlug, enterpriseTeam) + req, err := enterpriseTeamsNewRequest(client, "POST", u, enterpriseTeamOrgSlugsRequest{OrganizationSlugs: orgSlugs}) + if err != nil { + return nil, err + } + resp, err := client.Do(ctx, req, nil) + return resp, err +} + +func getEnterpriseTeamMembership(ctx context.Context, client *githubv3.Client, enterpriseSlug, enterpriseTeam, username string) (*githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams/%s/memberships/%s", enterpriseSlug, enterpriseTeam, username) + req, err := enterpriseTeamsNewRequest(client, "GET", u, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(ctx, req, nil) + return resp, err +} + +type enterpriseTeamMembership struct { + State string `json:"state"` + Role string `json:"role"` +} + +func parseEnterpriseTeamMembership(raw json.RawMessage) (*enterpriseTeamMembership, error) { + var m enterpriseTeamMembership + if err := json.Unmarshal(raw, &m); err == nil { + if m.State != "" || m.Role != "" { + return &m, nil + } + } + + var ms []enterpriseTeamMembership + if err := json.Unmarshal(raw, &ms); err == nil { + if len(ms) > 0 { + return &ms[0], nil + } + } + + // If the API ever returns an empty object, keep a non-nil struct for callers. + return &enterpriseTeamMembership{}, nil +} + +func getEnterpriseTeamMembershipDetails(ctx context.Context, client *githubv3.Client, enterpriseSlug, enterpriseTeam, username string) (*enterpriseTeamMembership, *githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams/%s/memberships/%s", enterpriseSlug, enterpriseTeam, username) + req, err := enterpriseTeamsNewRequest(client, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var raw json.RawMessage + resp, err := client.Do(ctx, req, &raw) + if err != nil { + return nil, resp, err + } + + m, err := parseEnterpriseTeamMembership(raw) + return m, resp, err +} + +func addEnterpriseTeamMember(ctx context.Context, client *githubv3.Client, enterpriseSlug, enterpriseTeam, username string) (*githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams/%s/memberships/%s", enterpriseSlug, enterpriseTeam, username) + req, err := enterpriseTeamsNewRequest(client, "PUT", u, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(ctx, req, nil) + return resp, err +} + +func removeEnterpriseTeamMember(ctx context.Context, client *githubv3.Client, enterpriseSlug, enterpriseTeam, username string) (*githubv3.Response, error) { + u := fmt.Sprintf("enterprises/%s/teams/%s/memberships/%s", enterpriseSlug, enterpriseTeam, username) + req, err := enterpriseTeamsNewRequest(client, "DELETE", u, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(ctx, req, nil) + return resp, err +} diff --git a/website/docs/d/enterprise_team.html.markdown b/website/docs/d/enterprise_team.html.markdown new file mode 100644 index 0000000000..e2e2bf2e97 --- /dev/null +++ b/website/docs/d/enterprise_team.html.markdown @@ -0,0 +1,49 @@ +--- +layout: "github" +page_title: "Github: github_enterprise_team" +description: |- + Get information about a GitHub enterprise team. +--- + +# github_enterprise_team + +Use this data source to retrieve information about an enterprise team. + +~> **Note:** Requires GitHub Enterprise Cloud with a classic PAT that has enterprise admin scope. + +## Example Usage + +Lookup by slug: + +```hcl +data "github_enterprise_team" "example" { + enterprise_slug = "my-enterprise" + slug = "ent:platform" +} +``` + +Lookup by numeric ID: + +```hcl +data "github_enterprise_team" "example" { + enterprise_slug = "my-enterprise" + team_id = 123456 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `slug` - (Optional) The slug of the enterprise team. Conflicts with `team_id`. +* `team_id` - (Optional) The numeric ID of the enterprise team. Conflicts with `slug`. + +## Attributes Reference + +The following additional attributes are exported: + +* `name` - The name of the enterprise team. +* `description` - The description of the enterprise team. +* `organization_selection_type` - Which organizations in the enterprise should have access to this team. +* `group_id` - The ID of the IdP group to assign team membership with. diff --git a/website/docs/d/enterprise_team_membership.html.markdown b/website/docs/d/enterprise_team_membership.html.markdown new file mode 100644 index 0000000000..53b380f31c --- /dev/null +++ b/website/docs/d/enterprise_team_membership.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "github" +page_title: "Github: github_enterprise_team_membership" +description: |- + Check if a user is a member of a GitHub enterprise team. +--- + +# github_enterprise_team_membership + +Use this data source to check whether a user belongs to an enterprise team. + +~> **Note:** Requires GitHub Enterprise Cloud with a classic PAT that has enterprise admin scope. + +## Example Usage + +```hcl +data "github_enterprise_team_membership" "example" { + enterprise_slug = "my-enterprise" + enterprise_team = "ent:platform" + username = "octocat" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `enterprise_team` - (Required) The slug or ID of the enterprise team. +* `username` - (Required) The GitHub username. + +## Attributes Reference + +The following additional attributes are exported: + +* `role` - The membership role, if returned by the API. +* `state` - The membership state, if returned by the API. +* `etag` - The response ETag. diff --git a/website/docs/d/enterprise_team_organizations.html.markdown b/website/docs/d/enterprise_team_organizations.html.markdown new file mode 100644 index 0000000000..a864d82979 --- /dev/null +++ b/website/docs/d/enterprise_team_organizations.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "github" +page_title: "Github: github_enterprise_team_organizations" +description: |- + Get organizations assigned to a GitHub enterprise team. +--- + +# github_enterprise_team_organizations + +Use this data source to retrieve the organizations that an enterprise team has access to. + +~> **Note:** Requires GitHub Enterprise Cloud with a classic PAT that has enterprise admin scope. + +## Example Usage + +```hcl +data "github_enterprise_team_organizations" "example" { + enterprise_slug = "my-enterprise" + enterprise_team = "ent:platform" +} + +output "assigned_orgs" { + value = data.github_enterprise_team_organizations.example.organization_slugs +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `enterprise_team` - (Required) The slug or ID of the enterprise team. + +## Attributes Reference + +The following additional attributes are exported: + +* `organization_slugs` - Set of organization slugs the enterprise team is assigned to. diff --git a/website/docs/d/enterprise_teams.html.markdown b/website/docs/d/enterprise_teams.html.markdown new file mode 100644 index 0000000000..1462c9c4a9 --- /dev/null +++ b/website/docs/d/enterprise_teams.html.markdown @@ -0,0 +1,45 @@ +--- +layout: "github" +page_title: "Github: github_enterprise_teams" +description: |- + List all enterprise teams in a GitHub enterprise. +--- + +# github_enterprise_teams + +Use this data source to retrieve all enterprise teams for an enterprise. + +~> **Note:** Requires GitHub Enterprise Cloud with a classic PAT that has enterprise admin scope. + +## Example Usage + +```hcl +data "github_enterprise_teams" "all" { + enterprise_slug = "my-enterprise" +} + +output "enterprise_team_slugs" { + value = [for t in data.github_enterprise_teams.all.teams : t.slug] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. + +## Attributes Reference + +The following additional attributes are exported: + +* `teams` - List of enterprise teams in the enterprise. + +Each `teams` element exports: + +* `team_id` - The numeric ID of the enterprise team. +* `slug` - The slug of the enterprise team. +* `name` - The name of the enterprise team. +* `description` - The description of the enterprise team. +* `organization_selection_type` - Which organizations in the enterprise should have access to this team. +* `group_id` - The ID of the IdP group to assign team membership with. diff --git a/website/docs/r/enterprise_team.html.markdown b/website/docs/r/enterprise_team.html.markdown new file mode 100644 index 0000000000..f33b35dae3 --- /dev/null +++ b/website/docs/r/enterprise_team.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "github" +page_title: "Github: github_enterprise_team" +description: |- + Create and manages a GitHub enterprise team. +--- + +# github_enterprise_team + +This resource allows you to create and manage a GitHub enterprise team. + +~> **Note:** These API endpoints are in public preview for GitHub Enterprise Cloud and require a classic personal access token with enterprise admin permissions. + +## Example Usage + +```hcl +data "github_enterprise" "enterprise" { + slug = "my-enterprise" +} + +resource "github_enterprise_team" "example" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "Platform" + description = "Platform Engineering" + organization_selection_type = "selected" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `name` - (Required) The name of the enterprise team. +* `description` - (Optional) A description of the enterprise team. +* `organization_selection_type` - (Optional) Which organizations in the enterprise should have access to this team. One of `disabled`, `selected`, or `all`. Defaults to `disabled`. +* `group_id` - (Optional) The ID of the IdP group to assign team membership with. + +## Attributes Reference + +The following additional attributes are exported: + +* `id` - The numeric ID of the enterprise team. +* `team_id` - The numeric ID of the enterprise team. +* `slug` - The slug of the enterprise team (GitHub generates it and adds the `ent:` prefix). + +## Import + +This resource can be imported using the enterprise slug and the enterprise team numeric ID: + +``` +$ terraform import github_enterprise_team.example enterprise-slug/42 +``` diff --git a/website/docs/r/enterprise_team_membership.html.markdown b/website/docs/r/enterprise_team_membership.html.markdown new file mode 100644 index 0000000000..e13e9ad3cf --- /dev/null +++ b/website/docs/r/enterprise_team_membership.html.markdown @@ -0,0 +1,47 @@ +--- +layout: "github" +page_title: "Github: github_enterprise_team_membership" +description: |- + Manages membership in a GitHub enterprise team. +--- + +# github_enterprise_team_membership + +This resource manages a user's membership in an enterprise team. + +~> **Note:** Requires GitHub Enterprise Cloud with a classic PAT that has enterprise admin scope. + +## Example Usage + +```hcl +data "github_enterprise" "enterprise" { + slug = "my-enterprise" +} + +resource "github_enterprise_team" "team" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "Platform" +} + +resource "github_enterprise_team_membership" "member" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.team.slug + username = "octocat" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `enterprise_team` - (Required) The slug or ID of the enterprise team. +* `username` - (Required) The GitHub username to manage. + +## Import + +This resource can be imported using: + +``` +$ terraform import github_enterprise_team_membership.member enterprise-slug/ent:platform/octocat +``` diff --git a/website/docs/r/enterprise_team_organizations.html.markdown b/website/docs/r/enterprise_team_organizations.html.markdown new file mode 100644 index 0000000000..a5ba4eb10d --- /dev/null +++ b/website/docs/r/enterprise_team_organizations.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "github" +page_title: "Github: github_enterprise_team_organizations" +description: |- + Manages organization assignments for a GitHub enterprise team. +--- + +# github_enterprise_team_organizations + +This resource manages which organizations an enterprise team is assigned to. It will reconcile +the current assignments with the desired `organization_slugs`, adding and removing as needed. + +~> **Note:** Requires GitHub Enterprise Cloud with a classic PAT that has enterprise admin scope. + +## Example Usage + +```hcl +data "github_enterprise" "enterprise" { + slug = "my-enterprise" +} + +resource "github_enterprise_team" "team" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "Platform" + organization_selection_type = "selected" +} + +resource "github_enterprise_team_organizations" "assignments" { + enterprise_slug = data.github_enterprise.enterprise.slug + enterprise_team = github_enterprise_team.team.slug + + organization_slugs = [ + "my-org", + "another-org", + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `enterprise_team` - (Required) The slug or ID of the enterprise team. +* `organization_slugs` - (Optional) Set of organization slugs to assign the team to. + +## Import + +This resource can be imported using: + +``` +$ terraform import github_enterprise_team_organizations.assignments enterprise-slug/ent:platform +``` From a5860375676d4808506fa4130b5028db91e01d01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:51:46 +0100 Subject: [PATCH 2/2] feat: migrate enterprise teams to context-aware CRUD --- github/data_source_github_enterprise_team.go | 53 ++++++++----- ...ource_github_enterprise_team_membership.go | 39 ++++++---- ...ce_github_enterprise_team_organizations.go | 25 ++++--- github/data_source_github_enterprise_teams.go | 19 +++-- github/resource_github_enterprise_team.go | 75 ++++++++++--------- ...ource_github_enterprise_team_membership.go | 39 +++++----- ...ce_github_enterprise_team_organizations.go | 50 +++++++------ 7 files changed, 171 insertions(+), 129 deletions(-) diff --git a/github/data_source_github_enterprise_team.go b/github/data_source_github_enterprise_team.go index 3eb0397e21..d0646962ec 100644 --- a/github/data_source_github_enterprise_team.go +++ b/github/data_source_github_enterprise_team.go @@ -6,12 +6,13 @@ import ( "strconv" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceGithubEnterpriseTeam() *schema.Resource { return &schema.Resource{ - Read: dataSourceGithubEnterpriseTeamRead, + ReadContext: dataSourceGithubEnterpriseTeamRead, Schema: map[string]*schema.Schema{ "enterprise_slug": { @@ -57,25 +58,23 @@ func dataSourceGithubEnterpriseTeam() *schema.Resource { } } -func dataSourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error { +func dataSourceGithubEnterpriseTeamRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) if enterpriseSlug == "" { - return fmt.Errorf("enterprise_slug must not be empty") + return diag.FromErr(fmt.Errorf("enterprise_slug must not be empty")) } - ctx := context.Background() - var te *enterpriseTeam if v, ok := d.GetOk("team_id"); ok { teamID := int64(v.(int)) if teamID != 0 { found, err := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) if err != nil { - return err + return diag.FromErr(err) } if found == nil { - return fmt.Errorf("could not find enterprise team %d in enterprise %s", teamID, enterpriseSlug) + return diag.FromErr(fmt.Errorf("could not find enterprise team %d in enterprise %s", teamID, enterpriseSlug)) } te = found } @@ -84,34 +83,52 @@ func dataSourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error if te == nil { teamSlug := strings.TrimSpace(d.Get("slug").(string)) if teamSlug == "" { - return fmt.Errorf("one of slug or team_id must be set") + return diag.FromErr(fmt.Errorf("one of slug or team_id must be set")) } found, _, err := getEnterpriseTeamBySlug(ctx, client, enterpriseSlug, teamSlug) if err != nil { - return err + return diag.FromErr(err) } te = found } d.SetId(buildSlashTwoPartID(enterpriseSlug, strconv.FormatInt(te.ID, 10))) - _ = d.Set("enterprise_slug", enterpriseSlug) - _ = d.Set("slug", te.Slug) - _ = d.Set("team_id", int(te.ID)) - _ = d.Set("name", te.Name) + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("slug", te.Slug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("team_id", int(te.ID)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("name", te.Name); err != nil { + return diag.FromErr(err) + } if te.Description != nil { - _ = d.Set("description", *te.Description) + if err := d.Set("description", *te.Description); err != nil { + return diag.FromErr(err) + } } else { - _ = d.Set("description", "") + if err := d.Set("description", ""); err != nil { + return diag.FromErr(err) + } } orgSel := te.OrganizationSelectionType if orgSel == "" { orgSel = "disabled" } - _ = d.Set("organization_selection_type", orgSel) + if err := d.Set("organization_selection_type", orgSel); err != nil { + return diag.FromErr(err) + } if te.GroupID != nil { - _ = d.Set("group_id", *te.GroupID) + if err := d.Set("group_id", *te.GroupID); err != nil { + return diag.FromErr(err) + } } else { - _ = d.Set("group_id", "") + if err := d.Set("group_id", ""); err != nil { + return diag.FromErr(err) + } } return nil diff --git a/github/data_source_github_enterprise_team_membership.go b/github/data_source_github_enterprise_team_membership.go index 8323dac424..9b55a12e5b 100644 --- a/github/data_source_github_enterprise_team_membership.go +++ b/github/data_source_github_enterprise_team_membership.go @@ -5,12 +5,13 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceGithubEnterpriseTeamMembership() *schema.Resource { return &schema.Resource{ - Read: dataSourceGithubEnterpriseTeamMembershipRead, + ReadContext: dataSourceGithubEnterpriseTeamMembershipRead, Schema: map[string]*schema.Schema{ "enterprise_slug": { @@ -47,37 +48,47 @@ func dataSourceGithubEnterpriseTeamMembership() *schema.Resource { } } -func dataSourceGithubEnterpriseTeamMembershipRead(d *schema.ResourceData, meta any) error { +func dataSourceGithubEnterpriseTeamMembershipRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) enterpriseTeam := strings.TrimSpace(d.Get("enterprise_team").(string)) username := strings.TrimSpace(d.Get("username").(string)) if enterpriseSlug == "" { - return fmt.Errorf("enterprise_slug must not be empty") + return diag.FromErr(fmt.Errorf("enterprise_slug must not be empty")) } if enterpriseTeam == "" { - return fmt.Errorf("enterprise_team must not be empty") + return diag.FromErr(fmt.Errorf("enterprise_team must not be empty")) } if username == "" { - return fmt.Errorf("username must not be empty") + return diag.FromErr(fmt.Errorf("username must not be empty")) } - - ctx := context.Background() m, resp, err := getEnterpriseTeamMembershipDetails(ctx, client, enterpriseSlug, enterpriseTeam, username) if err != nil { - return err + return diag.FromErr(err) } d.SetId(buildSlashThreePartID(enterpriseSlug, enterpriseTeam, username)) - _ = d.Set("enterprise_slug", enterpriseSlug) - _ = d.Set("enterprise_team", enterpriseTeam) - _ = d.Set("username", username) + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("enterprise_team", enterpriseTeam); err != nil { + return diag.FromErr(err) + } + if err := d.Set("username", username); err != nil { + return diag.FromErr(err) + } if m != nil { - _ = d.Set("role", m.Role) - _ = d.Set("state", m.State) + if err := d.Set("role", m.Role); err != nil { + return diag.FromErr(err) + } + if err := d.Set("state", m.State); err != nil { + return diag.FromErr(err) + } } if resp != nil { - _ = d.Set("etag", resp.Header.Get("ETag")) + if err := d.Set("etag", resp.Header.Get("ETag")); err != nil { + return diag.FromErr(err) + } } return nil } diff --git a/github/data_source_github_enterprise_team_organizations.go b/github/data_source_github_enterprise_team_organizations.go index ae690980e7..6dba903cd9 100644 --- a/github/data_source_github_enterprise_team_organizations.go +++ b/github/data_source_github_enterprise_team_organizations.go @@ -5,12 +5,13 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceGithubEnterpriseTeamOrganizations() *schema.Resource { return &schema.Resource{ - Read: dataSourceGithubEnterpriseTeamOrganizationsRead, + ReadContext: dataSourceGithubEnterpriseTeamOrganizationsRead, Schema: map[string]*schema.Schema{ "enterprise_slug": { @@ -34,21 +35,19 @@ func dataSourceGithubEnterpriseTeamOrganizations() *schema.Resource { } } -func dataSourceGithubEnterpriseTeamOrganizationsRead(d *schema.ResourceData, meta any) error { +func dataSourceGithubEnterpriseTeamOrganizationsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) enterpriseTeam := strings.TrimSpace(d.Get("enterprise_team").(string)) if enterpriseSlug == "" { - return fmt.Errorf("enterprise_slug must not be empty") + return diag.FromErr(fmt.Errorf("enterprise_slug must not be empty")) } if enterpriseTeam == "" { - return fmt.Errorf("enterprise_team must not be empty") + return diag.FromErr(fmt.Errorf("enterprise_team must not be empty")) } - - ctx := context.Background() orgs, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) if err != nil { - return err + return diag.FromErr(err) } slugs := make([]string, 0, len(orgs)) @@ -59,8 +58,14 @@ func dataSourceGithubEnterpriseTeamOrganizationsRead(d *schema.ResourceData, met } d.SetId(buildSlashTwoPartID(enterpriseSlug, enterpriseTeam)) - _ = d.Set("enterprise_slug", enterpriseSlug) - _ = d.Set("enterprise_team", enterpriseTeam) - _ = d.Set("organization_slugs", slugs) + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("enterprise_team", enterpriseTeam); err != nil { + return diag.FromErr(err) + } + if err := d.Set("organization_slugs", slugs); err != nil { + return diag.FromErr(err) + } return nil } diff --git a/github/data_source_github_enterprise_teams.go b/github/data_source_github_enterprise_teams.go index 1f774f0dee..a6eb498f1a 100644 --- a/github/data_source_github_enterprise_teams.go +++ b/github/data_source_github_enterprise_teams.go @@ -5,12 +5,13 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func dataSourceGithubEnterpriseTeams() *schema.Resource { return &schema.Resource{ - Read: dataSourceGithubEnterpriseTeamsRead, + ReadContext: dataSourceGithubEnterpriseTeamsRead, Schema: map[string]*schema.Schema{ "enterprise_slug": { @@ -61,17 +62,15 @@ func dataSourceGithubEnterpriseTeams() *schema.Resource { } } -func dataSourceGithubEnterpriseTeamsRead(d *schema.ResourceData, meta any) error { +func dataSourceGithubEnterpriseTeamsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) if enterpriseSlug == "" { - return fmt.Errorf("enterprise_slug must not be empty") + return diag.FromErr(fmt.Errorf("enterprise_slug must not be empty")) } - - ctx := context.Background() teams, err := listEnterpriseTeams(ctx, client, enterpriseSlug) if err != nil { - return err + return diag.FromErr(err) } flat := make([]any, 0, len(teams)) @@ -100,7 +99,11 @@ func dataSourceGithubEnterpriseTeamsRead(d *schema.ResourceData, meta any) error } d.SetId(enterpriseSlug) - _ = d.Set("enterprise_slug", enterpriseSlug) - _ = d.Set("teams", flat) + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("teams", flat); err != nil { + return diag.FromErr(err) + } return nil } diff --git a/github/resource_github_enterprise_team.go b/github/resource_github_enterprise_team.go index 11d19d01c9..751b58b331 100644 --- a/github/resource_github_enterprise_team.go +++ b/github/resource_github_enterprise_team.go @@ -10,6 +10,7 @@ import ( "strings" githubv3 "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -17,11 +18,11 @@ import ( func resourceGithubEnterpriseTeam() *schema.Resource { return &schema.Resource{ - Create: resourceGithubEnterpriseTeamCreate, - Read: resourceGithubEnterpriseTeamRead, - Update: resourceGithubEnterpriseTeamUpdate, - Delete: resourceGithubEnterpriseTeamDelete, - Importer: &schema.ResourceImporter{State: resourceGithubEnterpriseTeamImport}, + CreateContext: resourceGithubEnterpriseTeamCreate, + ReadContext: resourceGithubEnterpriseTeamRead, + UpdateContext: resourceGithubEnterpriseTeamUpdate, + DeleteContext: resourceGithubEnterpriseTeamDelete, + Importer: &schema.ResourceImporter{StateContext: resourceGithubEnterpriseTeamImport}, CustomizeDiff: customdiff.Sequence( customdiff.ComputedIf("slug", func(_ context.Context, d *schema.ResourceDiff, meta any) bool { @@ -75,7 +76,7 @@ func resourceGithubEnterpriseTeam() *schema.Resource { } } -func resourceGithubEnterpriseTeamCreate(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) @@ -93,26 +94,26 @@ func resourceGithubEnterpriseTeamCreate(d *schema.ResourceData, meta any) error req.GroupID = githubv3.String(groupID) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) te, _, err := createEnterpriseTeam(ctx, client, enterpriseSlug, req) if err != nil { - return err + return diag.FromErr(err) } d.SetId(strconv.FormatInt(te.ID, 10)) - return resourceGithubEnterpriseTeamRead(d, meta) + return resourceGithubEnterpriseTeamRead(context.WithValue(ctx, ctxId, d.Id()), d, meta) } -func resourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) teamID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) // Try to fetch by slug first (faster), but if the team was renamed we need // to fall back to listing all teams and matching by numeric ID. @@ -125,7 +126,7 @@ func resourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error { } else { ghErr := &githubv3.ErrorResponse{} if errors.As(getErr, &ghErr) && ghErr.Response.StatusCode != http.StatusNotFound { - return getErr + return diag.FromErr(getErr) } } } @@ -134,7 +135,7 @@ func resourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error { if te == nil { te, err = findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) if err != nil { - return err + return diag.FromErr(err) } if te == nil { log.Printf("[INFO] Removing enterprise team %s/%s from state because it no longer exists in GitHub", enterpriseSlug, d.Id()) @@ -144,47 +145,47 @@ func resourceGithubEnterpriseTeamRead(d *schema.ResourceData, meta any) error { } if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("name", te.Name); err != nil { - return err + return diag.FromErr(err) } if te.Description != nil { if err = d.Set("description", *te.Description); err != nil { - return err + return diag.FromErr(err) } } else { if err = d.Set("description", ""); err != nil { - return err + return diag.FromErr(err) } } if err = d.Set("slug", te.Slug); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("team_id", int(te.ID)); err != nil { - return err + return diag.FromErr(err) } orgSelection := te.OrganizationSelectionType if orgSelection == "" { orgSelection = "disabled" } if err = d.Set("organization_selection_type", orgSelection); err != nil { - return err + return diag.FromErr(err) } if te.GroupID != nil { if err = d.Set("group_id", *te.GroupID); err != nil { - return err + return diag.FromErr(err) } } else { if err = d.Set("group_id", ""); err != nil { - return err + return diag.FromErr(err) } } return nil } -func resourceGithubEnterpriseTeamUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) @@ -193,15 +194,15 @@ func resourceGithubEnterpriseTeamUpdate(d *schema.ResourceData, meta any) error if teamSlug == "" { teamID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) te, err := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) if err != nil { - return err + return diag.FromErr(err) } if te == nil { - return fmt.Errorf("enterprise team %s no longer exists", d.Id()) + return diag.FromErr(fmt.Errorf("enterprise team %s no longer exists", d.Id())) } teamSlug = te.Slug } @@ -220,29 +221,29 @@ func resourceGithubEnterpriseTeamUpdate(d *schema.ResourceData, meta any) error req.GroupID = githubv3.String(groupID) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, _, err := updateEnterpriseTeam(ctx, client, enterpriseSlug, teamSlug, req) if err != nil { - return err + return diag.FromErr(err) } - return resourceGithubEnterpriseTeamRead(d, meta) + return resourceGithubEnterpriseTeamRead(ctx, d, meta) } -func resourceGithubEnterpriseTeamDelete(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) teamSlug := strings.TrimSpace(d.Get("slug").(string)) if teamSlug == "" { teamID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } te, err := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) if err != nil { - return err + return diag.FromErr(err) } if te == nil { return nil @@ -259,13 +260,13 @@ func resourceGithubEnterpriseTeamDelete(d *schema.ResourceData, meta any) error return nil } _ = resp - return err + return diag.FromErr(err) } return nil } -func resourceGithubEnterpriseTeamImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { +func resourceGithubEnterpriseTeamImport(_ context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { // Import format: / parts := strings.Split(d.Id(), "/") if len(parts) != 2 { diff --git a/github/resource_github_enterprise_team_membership.go b/github/resource_github_enterprise_team_membership.go index 1719ca232b..46e75d4476 100644 --- a/github/resource_github_enterprise_team_membership.go +++ b/github/resource_github_enterprise_team_membership.go @@ -8,15 +8,16 @@ import ( "net/http" githubv3 "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubEnterpriseTeamMembership() *schema.Resource { return &schema.Resource{ - Create: resourceGithubEnterpriseTeamMembershipCreate, - Read: resourceGithubEnterpriseTeamMembershipRead, - Delete: resourceGithubEnterpriseTeamMembershipDelete, - Importer: &schema.ResourceImporter{State: resourceGithubEnterpriseTeamMembershipImport}, + CreateContext: resourceGithubEnterpriseTeamMembershipCreate, + ReadContext: resourceGithubEnterpriseTeamMembershipRead, + DeleteContext: resourceGithubEnterpriseTeamMembershipDelete, + Importer: &schema.ResourceImporter{StateContext: resourceGithubEnterpriseTeamMembershipImport}, Schema: map[string]*schema.Schema{ "enterprise_slug": { @@ -42,44 +43,44 @@ func resourceGithubEnterpriseTeamMembership() *schema.Resource { } } -func resourceGithubEnterpriseTeamMembershipCreate(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamMembershipCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) enterpriseTeam := d.Get("enterprise_team").(string) username := d.Get("username").(string) - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) // The API is idempotent, so we don't need to check if they're already a member _, err := addEnterpriseTeamMember(ctx, client, enterpriseSlug, enterpriseTeam, username) if err != nil { - return err + return diag.FromErr(err) } // NOTE: enterprise team slugs have the "ent:" prefix, so we must not use // colon-delimited IDs here. d.SetId(buildSlashThreePartID(enterpriseSlug, enterpriseTeam, username)) - return resourceGithubEnterpriseTeamMembershipRead(d, meta) + return resourceGithubEnterpriseTeamMembershipRead(context.WithValue(ctx, ctxId, d.Id()), d, meta) } -func resourceGithubEnterpriseTeamMembershipRead(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamMembershipRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug, enterpriseTeam, username, err := parseSlashThreePartID(d.Id(), "enterprise_slug", "enterprise_team", "username") if err != nil { - return err + return diag.FromErr(err) } if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("enterprise_team", enterpriseTeam); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("username", username); err != nil { - return err + return diag.FromErr(err) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, err = getEnterpriseTeamMembership(ctx, client, enterpriseSlug, enterpriseTeam, username) if err != nil { ghErr := &githubv3.ErrorResponse{} @@ -90,19 +91,19 @@ func resourceGithubEnterpriseTeamMembershipRead(d *schema.ResourceData, meta any return nil } } - return err + return diag.FromErr(err) } return nil } -func resourceGithubEnterpriseTeamMembershipDelete(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamMembershipDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) enterpriseTeam := d.Get("enterprise_team").(string) username := d.Get("username").(string) - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) resp, err := removeEnterpriseTeamMember(ctx, client, enterpriseSlug, enterpriseTeam, username) if err != nil { ghErr := &githubv3.ErrorResponse{} @@ -110,13 +111,13 @@ func resourceGithubEnterpriseTeamMembershipDelete(d *schema.ResourceData, meta a return nil } _ = resp - return err + return diag.FromErr(err) } return nil } -func resourceGithubEnterpriseTeamMembershipImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { +func resourceGithubEnterpriseTeamMembershipImport(_ context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { enterpriseSlug, enterpriseTeam, username, err := parseSlashThreePartID(d.Id(), "enterprise_slug", "enterprise_team", "username") if err != nil { return nil, fmt.Errorf("invalid import specified: supplied import must be written as //") diff --git a/github/resource_github_enterprise_team_organizations.go b/github/resource_github_enterprise_team_organizations.go index bb0c739ec2..9e4881e59a 100644 --- a/github/resource_github_enterprise_team_organizations.go +++ b/github/resource_github_enterprise_team_organizations.go @@ -9,16 +9,17 @@ import ( "strings" githubv3 "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubEnterpriseTeamOrganizations() *schema.Resource { return &schema.Resource{ - Create: resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate, - Read: resourceGithubEnterpriseTeamOrganizationsRead, - Update: resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate, - Delete: resourceGithubEnterpriseTeamOrganizationsDelete, - Importer: &schema.ResourceImporter{State: resourceGithubEnterpriseTeamOrganizationsImport}, + CreateContext: resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate, + ReadContext: resourceGithubEnterpriseTeamOrganizationsRead, + UpdateContext: resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate, + DeleteContext: resourceGithubEnterpriseTeamOrganizationsDelete, + Importer: &schema.ResourceImporter{StateContext: resourceGithubEnterpriseTeamOrganizationsImport}, Schema: map[string]*schema.Schema{ "enterprise_slug": { @@ -44,7 +45,7 @@ func resourceGithubEnterpriseTeamOrganizations() *schema.Resource { } } -func resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) enterpriseTeam := d.Get("enterprise_team").(string) @@ -59,10 +60,10 @@ func resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate(d *schema.ResourceD } } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) current, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) if err != nil { - return err + return diag.FromErr(err) } currentSet := map[string]struct{}{} @@ -88,33 +89,33 @@ func resourceGithubEnterpriseTeamOrganizationsCreateOrUpdate(d *schema.ResourceD // Perform adds before removes to avoid transient states where the team has no orgs if err := addEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam, toAdd); err != nil { - return err + return diag.FromErr(err) } if _, err := removeEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam, toRemove); err != nil { - return err + return diag.FromErr(err) } // NOTE: enterprise team slugs have the "ent:" prefix, so we must not use // colon-delimited IDs here. d.SetId(buildSlashTwoPartID(enterpriseSlug, enterpriseTeam)) - return resourceGithubEnterpriseTeamOrganizationsRead(d, meta) + return resourceGithubEnterpriseTeamOrganizationsRead(context.WithValue(ctx, ctxId, d.Id()), d, meta) } -func resourceGithubEnterpriseTeamOrganizationsRead(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamOrganizationsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug, enterpriseTeam, err := parseSlashTwoPartID(d.Id(), "enterprise_slug", "enterprise_team") if err != nil { - return err + return diag.FromErr(err) } if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("enterprise_team", enterpriseTeam); err != nil { - return err + return diag.FromErr(err) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) orgs, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) if err != nil { ghErr := &githubv3.ErrorResponse{} @@ -125,7 +126,7 @@ func resourceGithubEnterpriseTeamOrganizationsRead(d *schema.ResourceData, meta return nil } } - return err + return diag.FromErr(err) } slugs := []string{} @@ -135,25 +136,25 @@ func resourceGithubEnterpriseTeamOrganizationsRead(d *schema.ResourceData, meta } } if err = d.Set("organization_slugs", slugs); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubEnterpriseTeamOrganizationsDelete(d *schema.ResourceData, meta any) error { +func resourceGithubEnterpriseTeamOrganizationsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client enterpriseSlug := d.Get("enterprise_slug").(string) enterpriseTeam := d.Get("enterprise_team").(string) - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) orgs, err := listEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam) if err != nil { ghErr := &githubv3.ErrorResponse{} if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { return nil } - return err + return diag.FromErr(err) } toRemove := []string{} @@ -165,10 +166,13 @@ func resourceGithubEnterpriseTeamOrganizationsDelete(d *schema.ResourceData, met log.Printf("[INFO] Removing all organization assignments for enterprise team: %s/%s", enterpriseSlug, enterpriseTeam) _, err = removeEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, enterpriseTeam, toRemove) - return err + if err != nil { + return diag.FromErr(err) + } + return nil } -func resourceGithubEnterpriseTeamOrganizationsImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { +func resourceGithubEnterpriseTeamOrganizationsImport(_ context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { enterpriseSlug, enterpriseTeam, err := parseSlashTwoPartID(d.Id(), "enterprise_slug", "enterprise_team") if err != nil { return nil, fmt.Errorf("invalid import specified: supplied import must be written as /")