diff --git a/github/data_source_github_enterprise_team.go b/github/data_source_github_enterprise_team.go new file mode 100644 index 0000000000..27f71b0957 --- /dev/null +++ b/github/data_source_github_enterprise_team.go @@ -0,0 +1,140 @@ +package github + +import ( + "context" + "strconv" + "strings" + + "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceGithubEnterpriseTeam() *schema.Resource { + return &schema.Resource{ + Description: "Gets information about a GitHub enterprise team.", + ReadContext: dataSourceGithubEnterpriseTeamRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "slug": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"slug", "team_id"}, + Description: "The slug of the enterprise team.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "team_id": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"slug", "team_id"}, + Description: "The numeric ID of the enterprise team.", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), + }, + "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(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + + var te *github.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 diag.FromErr(err) + } + if found == nil { + return diag.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 diag.Errorf("one of slug or team_id must be set") + } + found, _, err := client.Enterprise.GetTeam(ctx, enterpriseSlug, teamSlug) + if err != nil { + return diag.FromErr(err) + } + te = found + } + + d.SetId(buildTwoPartID(enterpriseSlug, strconv.FormatInt(te.ID, 10))) + 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 { + if err := d.Set("description", *te.Description); err != nil { + return diag.FromErr(err) + } + } else { + if err := d.Set("description", ""); err != nil { + return diag.FromErr(err) + } + } + orgSel := "" + if te.OrganizationSelectionType != nil { + orgSel = *te.OrganizationSelectionType + } + if orgSel == "" { + orgSel = "disabled" + } + if err := d.Set("organization_selection_type", orgSel); err != nil { + return diag.FromErr(err) + } + if te.GroupID != "" { + if err := d.Set("group_id", te.GroupID); err != nil { + return diag.FromErr(err) + } + } else { + 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 new file mode 100644 index 0000000000..f61b335f6b --- /dev/null +++ b/github/data_source_github_enterprise_team_membership.go @@ -0,0 +1,74 @@ +package github + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceGithubEnterpriseTeamMembership() *schema.Resource { + return &schema.Resource{ + Description: "Retrieves information about a user's membership in a GitHub enterprise team.", + ReadContext: dataSourceGithubEnterpriseTeamMembershipRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "team_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise team.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "username": { + Type: schema.TypeString, + Required: true, + Description: "The username of the user.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "user_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the user.", + }, + }, + } +} + +func dataSourceGithubEnterpriseTeamMembershipRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + teamSlug := strings.TrimSpace(d.Get("team_slug").(string)) + username := strings.TrimSpace(d.Get("username").(string)) + + // Get the membership using the SDK + user, _, err := client.Enterprise.GetTeamMembership(ctx, enterpriseSlug, teamSlug, username) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildEnterpriseTeamMembershipID(enterpriseSlug, teamSlug, username)) + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("team_slug", teamSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("username", username); err != nil { + return diag.FromErr(err) + } + if user != nil && user.ID != nil { + if err := d.Set("user_id", int(*user.ID)); 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 new file mode 100644 index 0000000000..dcd770ca91 --- /dev/null +++ b/github/data_source_github_enterprise_team_organizations.go @@ -0,0 +1,68 @@ +package github + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceGithubEnterpriseTeamOrganizations() *schema.Resource { + return &schema.Resource{ + Description: "Lists organizations assigned to a GitHub enterprise team.", + ReadContext: dataSourceGithubEnterpriseTeamOrganizationsRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "team_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise team.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "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(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + teamSlug := strings.TrimSpace(d.Get("team_slug").(string)) + orgs, err := listAllEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, teamSlug) + if err != nil { + return diag.FromErr(err) + } + + slugs := make([]string, 0, len(orgs)) + for _, org := range orgs { + if org.Login != nil && *org.Login != "" { + slugs = append(slugs, *org.Login) + } + } + + d.SetId(buildEnterpriseTeamOrganizationsID(enterpriseSlug, teamSlug)) + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err := d.Set("team_slug", teamSlug); 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_team_test.go b/github/data_source_github_enterprise_team_test.go new file mode 100644 index 0000000000..eb688bdd8a --- /dev/null +++ b/github/data_source_github_enterprise_team_test.go @@ -0,0 +1,178 @@ +package github + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubEnterpriseTeamDataSource(t *testing.T) { + t.Run("retrieves team by slug without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + } + + data "github_enterprise_team" "by_slug" { + enterprise_slug = data.github_enterprise.enterprise.slug + slug = github_enterprise_team.test.slug + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_team.by_slug", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.CompareValuePairs("data.github_enterprise_team.by_slug", tfjsonpath.New("team_id"), "github_enterprise_team.test", tfjsonpath.New("team_id"), compare.ValuesSame()), + statecheck.CompareValuePairs("data.github_enterprise_team.by_slug", tfjsonpath.New("slug"), "github_enterprise_team.test", tfjsonpath.New("slug"), compare.ValuesSame()), + statecheck.CompareValuePairs("data.github_enterprise_team.by_slug", tfjsonpath.New("name"), "github_enterprise_team.test", tfjsonpath.New("name"), compare.ValuesSame()), + }, + }, + }, + }) + }) + + t.Run("retrieves team by id without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + } + + data "github_enterprise_team" "by_id" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_id = github_enterprise_team.test.team_id + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_team.by_id", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.CompareValuePairs("data.github_enterprise_team.by_id", tfjsonpath.New("team_id"), "github_enterprise_team.test", tfjsonpath.New("team_id"), compare.ValuesSame()), + statecheck.CompareValuePairs("data.github_enterprise_team.by_id", tfjsonpath.New("slug"), "github_enterprise_team.test", tfjsonpath.New("slug"), compare.ValuesSame()), + }, + }, + }, + }) + }) +} + +func TestAccGithubEnterpriseTeamOrganizationsDataSource(t *testing.T) { + orgSlug := os.Getenv("ENTERPRISE_TEST_ORGANIZATION") + if orgSlug == "" { + t.Skip("ENTERPRISE_TEST_ORGANIZATION not set") + } + + t.Run("retrieves team organizations without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + organization_selection_type = "selected" + } + + resource "github_enterprise_team_organizations" "assign" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + organization_slugs = [%q] + } + + data "github_enterprise_team_organizations" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + depends_on = [github_enterprise_team_organizations.assign] + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID, orgSlug), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_team_organizations.test", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("data.github_enterprise_team_organizations.test", tfjsonpath.New("organization_slugs"), knownvalue.SetSizeExact(1)), + statecheck.ExpectKnownValue("data.github_enterprise_team_organizations.test", tfjsonpath.New("organization_slugs"), knownvalue.SetPartial([]knownvalue.Check{knownvalue.StringExact(orgSlug)})), + }, + }, + }, + }) + }) +} + +func TestAccGithubEnterpriseTeamMembershipDataSource(t *testing.T) { + username := os.Getenv("ENTERPRISE_TEST_USER") + if username == "" { + t.Skip("ENTERPRISE_TEST_USER not set") + } + + t.Run("retrieves team membership without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + } + + resource "github_enterprise_team_membership" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + username = %q + } + + data "github_enterprise_team_membership" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + username = %q + depends_on = [github_enterprise_team_membership.test] + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID, username, username), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_team_membership.test", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("data.github_enterprise_team_membership.test", tfjsonpath.New("username"), knownvalue.StringExact(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..244874fc23 --- /dev/null +++ b/github/data_source_github_enterprise_teams.go @@ -0,0 +1,120 @@ +package github + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +const ( + teamIDKey = "team_id" + teamSlugKey = "slug" + teamNameKey = "name" + teamDescriptionKey = "description" + teamOrganizationSelectionKey = "organization_selection_type" + teamGroupIDKey = "group_id" +) + +func dataSourceGithubEnterpriseTeams() *schema.Resource { + return &schema.Resource{ + Description: "Lists all GitHub enterprise teams in an enterprise.", + ReadContext: dataSourceGithubEnterpriseTeamsRead, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "teams": { + Type: schema.TypeList, + Computed: true, + Description: "All teams in the enterprise.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + teamIDKey: { + Type: schema.TypeInt, + Computed: true, + Description: "The numeric ID of the enterprise team.", + }, + teamSlugKey: { + Type: schema.TypeString, + Computed: true, + Description: "The slug of the enterprise team.", + }, + teamNameKey: { + Type: schema.TypeString, + Computed: true, + Description: "The name of the enterprise team.", + }, + teamDescriptionKey: { + Type: schema.TypeString, + Computed: true, + Description: "A description of the enterprise team.", + }, + teamOrganizationSelectionKey: { + Type: schema.TypeString, + Computed: true, + Description: "Specifies which organizations in the enterprise should have access to this team.", + }, + teamGroupIDKey: { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the IdP group to assign team membership with.", + }, + }, + }, + }, + }, + } +} + +func dataSourceGithubEnterpriseTeamsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + teams, err := listAllEnterpriseTeams(ctx, client, enterpriseSlug) + if err != nil { + return diag.FromErr(err) + } + + flat := make([]any, 0, len(teams)) + for _, team := range teams { + m := map[string]any{ + teamIDKey: int(team.ID), + teamSlugKey: team.Slug, + teamNameKey: team.Name, + } + if team.Description != nil { + m[teamDescriptionKey] = *team.Description + } else { + m[teamDescriptionKey] = "" + } + orgSel := "" + if team.OrganizationSelectionType != nil { + orgSel = *team.OrganizationSelectionType + } + if orgSel == "" { + orgSel = "disabled" + } + m[teamOrganizationSelectionKey] = orgSel + if team.GroupID != "" { + m[teamGroupIDKey] = team.GroupID + } else { + m[teamGroupIDKey] = "" + } + flat = append(flat, m) + } + + d.SetId(enterpriseSlug) + 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/data_source_github_enterprise_teams_test.go b/github/data_source_github_enterprise_teams_test.go new file mode 100644 index 0000000000..e4acb89349 --- /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-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubEnterpriseTeamsDataSource(t *testing.T) { + t.Run("lists all enterprise teams without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + } + + data "github_enterprise_teams" "all" { + enterprise_slug = data.github_enterprise.enterprise.slug + depends_on = [github_enterprise_team.test] + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("data.github_enterprise_teams.all", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("data.github_enterprise_teams.all", tfjsonpath.New("teams"), knownvalue.ListPartial(map[int]knownvalue.Check{ + 0: knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "team_id": knownvalue.NotNull(), + "slug": knownvalue.NotNull(), + "name": knownvalue.NotNull(), + }), + })), + }, + }, + }, + }) + }) +} diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..e0b1fbf83f 100644 --- a/github/provider.go +++ b/github/provider.go @@ -213,6 +213,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_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(), @@ -294,6 +297,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..49d815c57c --- /dev/null +++ b/github/resource_github_enterprise_team.go @@ -0,0 +1,281 @@ +package github + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "strconv" + "strings" + + "github.com/google/go-github/v83/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" +) + +func resourceGithubEnterpriseTeam() *schema.Resource { + return &schema.Resource{ + Description: "Manages a GitHub enterprise team.", + 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 { + 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).", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 255)), + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the enterprise team.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 255)), + }, + "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: validation.ToDiagFunc(validation.StringInSlice([]string{"disabled", "selected", "all"}, false)), + }, + "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(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + 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 := github.EnterpriseTeamCreateOrUpdateRequest{ + Name: name, + OrganizationSelectionType: github.Ptr(orgSelection), + GroupID: github.Ptr(groupID), // Empty string is valid for no group + } + if description != "" { + req.Description = github.Ptr(description) + } + + ctx = context.WithValue(ctx, ctxId, d.Id()) + te, _, err := client.Enterprise.CreateTeam(ctx, enterpriseSlug, req) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.FormatInt(te.ID, 10)) + + // Set computed fields directly from API response + 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) + } + + return nil +} + +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 diag.FromErr(unconvertibleIdErr(d.Id(), err)) + } + + 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. + var te *github.EnterpriseTeam + if slug, ok := d.GetOk("slug"); ok { + if s := strings.TrimSpace(slug.(string)); s != "" { + candidate, _, getErr := client.Enterprise.GetTeam(ctx, enterpriseSlug, s) + if getErr == nil { + te = candidate + } else { + ghErr := &github.ErrorResponse{} + if errors.As(getErr, &ghErr) && ghErr.Response.StatusCode != http.StatusNotFound { + return diag.FromErr(getErr) + } + } + } + } + + if te == nil { + te, err = findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if err != nil { + 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()) + d.SetId("") + return nil + } + } + + if err = d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + if err = d.Set("name", te.Name); err != nil { + return diag.FromErr(err) + } + if te.Description != nil { + if err = d.Set("description", *te.Description); err != nil { + return diag.FromErr(err) + } + } else { + if err = d.Set("description", ""); 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) + } + orgSelection := "" + if te.OrganizationSelectionType != nil { + orgSelection = *te.OrganizationSelectionType + } + if orgSelection == "" { + orgSelection = "disabled" + } + if err = d.Set("organization_selection_type", orgSelection); err != nil { + return diag.FromErr(err) + } + if te.GroupID != "" { + if err = d.Set("group_id", te.GroupID); err != nil { + return diag.FromErr(err) + } + } else { + if err = d.Set("group_id", ""); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceGithubEnterpriseTeamUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := d.Get("enterprise_slug").(string) + teamSlug := d.Get("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 := github.EnterpriseTeamCreateOrUpdateRequest{ + Name: name, + OrganizationSelectionType: github.Ptr(orgSelection), + GroupID: github.Ptr(groupID), // Empty string clears the group + } + if description != "" { + req.Description = github.Ptr(description) + } + + ctx = context.WithValue(ctx, ctxId, d.Id()) + te, _, err := client.Enterprise.UpdateTeam(ctx, enterpriseSlug, teamSlug, req) + if err != nil { + return diag.FromErr(err) + } + + // Update slug in case it changed (e.g., team was renamed) + if err := d.Set("slug", te.Slug); err != nil { + return diag.FromErr(err) + } + + return nil +} + +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(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 diag.FromErr(unconvertibleIdErr(d.Id(), err)) + } + te, err := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if err != nil { + return diag.FromErr(err) + } + if te == nil { + return nil + } + teamSlug = te.Slug + } + + log.Printf("[INFO] Deleting enterprise team: %s/%s (%s)", enterpriseSlug, teamSlug, d.Id()) + _, err := client.Enterprise.DeleteTeam(ctx, enterpriseSlug, teamSlug) + if err != nil { + // Already gone? That's fine, we wanted it deleted anyway. + ghErr := &github.ErrorResponse{} + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubEnterpriseTeamImport(_ context.Context, 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) + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return nil, err + } + 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..dd0958dd73 --- /dev/null +++ b/github/resource_github_enterprise_team_membership.go @@ -0,0 +1,206 @@ +package github + +import ( + "context" + "errors" + "net/http" + "strings" + + "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubEnterpriseTeamMembership() *schema.Resource { + return &schema.Resource{ + Description: "Manages membership of a user in a GitHub enterprise team.", + CreateContext: resourceGithubEnterpriseTeamMembershipCreate, + ReadContext: resourceGithubEnterpriseTeamMembershipRead, + DeleteContext: resourceGithubEnterpriseTeamMembershipDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "team_slug": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The slug of the enterprise team.", + ExactlyOneOf: []string{"team_slug", "team_id"}, + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "team_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The ID of the enterprise team.", + ExactlyOneOf: []string{"team_slug", "team_id"}, + }, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The username of the user to add to the team.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "user_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the user.", + }, + }, + } +} + +func resourceGithubEnterpriseTeamMembershipCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + username := strings.TrimSpace(d.Get("username").(string)) + + var team *github.EnterpriseTeam + var err error + if v, ok := d.GetOk("team_slug"); ok { + team, _, err = client.Enterprise.GetTeam(ctx, enterpriseSlug, v.(string)) + } else { + teamID := int64(d.Get("team_id").(int)) + team, err = findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + } + if err != nil { + return diag.FromErr(err) + } + if team == nil { + return diag.Errorf("enterprise team not found") + } + + user, _, err := client.Enterprise.AddTeamMember(ctx, enterpriseSlug, team.Slug, username) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildEnterpriseTeamMembershipID(enterpriseSlug, team.Slug, username)) + + // Only set team_slug or team_id based on what user provided + if _, ok := d.GetOk("team_slug"); ok { + if err := d.Set("team_slug", team.Slug); err != nil { + return diag.FromErr(err) + } + } else if _, ok := d.GetOk("team_id"); ok { + if err := d.Set("team_id", int(team.ID)); err != nil { + return diag.FromErr(err) + } + } + + if user != nil && user.ID != nil { + if err := d.Set("user_id", int(*user.ID)); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceGithubEnterpriseTeamMembershipRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug, teamSlug, username, err := parseEnterpriseTeamMembershipID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + if v, ok := d.GetOk("team_id"); ok { + teamID := int64(v.(int)) + if teamID > 0 { + team, findErr := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if findErr != nil { + return diag.FromErr(findErr) + } + if team == nil { + d.SetId("") + return nil + } + teamSlug = team.Slug + } + } + + user, resp, err := client.Enterprise.GetTeamMembership(ctx, enterpriseSlug, teamSlug, username) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + if resp != nil && resp.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + // Only set team_slug if it was configured, or if neither team_slug nor team_id + // is present (e.g., during import). This avoids drift when users configure team_id. + if _, ok := d.GetOk("team_slug"); ok { + if err := d.Set("team_slug", teamSlug); err != nil { + return diag.FromErr(err) + } + } else if _, ok := d.GetOk("team_id"); !ok { + // During import, neither is set, so we populate team_slug + if err := d.Set("team_slug", teamSlug); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("username", username); err != nil { + return diag.FromErr(err) + } + if user != nil && user.ID != nil { + if err := d.Set("user_id", int(*user.ID)); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceGithubEnterpriseTeamMembershipDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug, teamSlug, username, err := parseEnterpriseTeamMembershipID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + if v, ok := d.GetOk("team_id"); ok { + teamID := int64(v.(int)) + if teamID > 0 { + team, findErr := findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + if findErr != nil { + return diag.FromErr(findErr) + } + if team == nil { + return nil + } + teamSlug = team.Slug + } + } + + resp, err := client.Enterprise.RemoveTeamMember(ctx, enterpriseSlug, teamSlug, username) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) + } + + return 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..e839050e2e --- /dev/null +++ b/github/resource_github_enterprise_team_organizations.go @@ -0,0 +1,236 @@ +package github + +import ( + "context" + "errors" + "net/http" + "strings" + + "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubEnterpriseTeamOrganizations() *schema.Resource { + return &schema.Resource{ + Description: "Manages organization assignments for a GitHub enterprise team.", + CreateContext: resourceGithubEnterpriseTeamOrganizationsCreate, + ReadContext: resourceGithubEnterpriseTeamOrganizationsRead, + UpdateContext: resourceGithubEnterpriseTeamOrganizationsUpdate, + DeleteContext: resourceGithubEnterpriseTeamOrganizationsDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the enterprise.", + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "team_slug": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The slug of the enterprise team.", + ExactlyOneOf: []string{"team_slug", "team_id"}, + ValidateDiagFunc: validation.ToDiagFunc(validation.All(validation.StringIsNotWhiteSpace, validation.StringIsNotEmpty)), + }, + "team_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The ID of the enterprise team.", + ExactlyOneOf: []string{"team_slug", "team_id"}, + }, + "organization_slugs": { + Type: schema.TypeSet, + Required: true, + Description: "Set of organization slugs that the enterprise team should be assigned to.", + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + MinItems: 1, + }, + }, + } +} + +func resourceGithubEnterpriseTeamOrganizationsCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug := strings.TrimSpace(d.Get("enterprise_slug").(string)) + + var team *github.EnterpriseTeam + var err error + if v, ok := d.GetOk("team_slug"); ok { + team, _, err = client.Enterprise.GetTeam(ctx, enterpriseSlug, v.(string)) + } else { + teamID := int64(d.Get("team_id").(int)) + team, err = findEnterpriseTeamByID(ctx, client, enterpriseSlug, teamID) + } + if err != nil { + return diag.FromErr(err) + } + if team == nil { + return diag.Errorf("enterprise team not found") + } + + // Verify no organizations are already assigned (authoritative resource) + existing, err := listAllEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, team.Slug) + if err != nil { + return diag.FromErr(err) + } + if len(existing) > 0 { + return diag.Errorf("%q already has organizations assigned; import first or remove manually", team.Slug) + } + + orgSlugsSet := d.Get("organization_slugs").(*schema.Set) + orgSlugs := make([]string, 0, orgSlugsSet.Len()) + for _, item := range orgSlugsSet.List() { + orgSlugs = append(orgSlugs, item.(string)) + } + + _, _, err = client.Enterprise.AddMultipleAssignments(ctx, enterpriseSlug, team.Slug, orgSlugs) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(buildEnterpriseTeamOrganizationsID(enterpriseSlug, team.Slug)) + + // Only set team_slug or team_id based on what user provided + if _, ok := d.GetOk("team_slug"); ok { + if err := d.Set("team_slug", team.Slug); err != nil { + return diag.FromErr(err) + } + } else if _, ok := d.GetOk("team_id"); ok { + if err := d.Set("team_id", int(team.ID)); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceGithubEnterpriseTeamOrganizationsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug, teamSlug, err := parseEnterpriseTeamOrganizationsID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + orgs, err := listAllEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, teamSlug) + if err != nil { + return diag.FromErr(err) + } + + slugs := make([]string, 0, len(orgs)) + for _, org := range orgs { + if org.Login != nil && *org.Login != "" { + slugs = append(slugs, *org.Login) + } + } + + if err := d.Set("enterprise_slug", enterpriseSlug); err != nil { + return diag.FromErr(err) + } + // Only set team_slug if it was configured, or if neither team_slug nor team_id + // is present (e.g., during import). This avoids drift when users configure team_id. + if _, ok := d.GetOk("team_slug"); ok { + if err := d.Set("team_slug", teamSlug); err != nil { + return diag.FromErr(err) + } + } else if _, ok := d.GetOk("team_id"); !ok { + // During import, neither is set, so we populate team_slug + if err := d.Set("team_slug", teamSlug); err != nil { + return diag.FromErr(err) + } + } + if err := d.Set("organization_slugs", slugs); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubEnterpriseTeamOrganizationsUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug, teamSlug, err := parseEnterpriseTeamOrganizationsID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("organization_slugs") { + oldVal, newVal := d.GetChange("organization_slugs") + oldSet := oldVal.(*schema.Set) + newSet := newVal.(*schema.Set) + + toAdd := newSet.Difference(oldSet) + toRemove := oldSet.Difference(newSet) + + if toAdd.Len() > 0 { + addSlugs := make([]string, 0, toAdd.Len()) + for _, item := range toAdd.List() { + addSlugs = append(addSlugs, item.(string)) + } + _, _, err = client.Enterprise.AddMultipleAssignments(ctx, enterpriseSlug, teamSlug, addSlugs) + if err != nil { + return diag.FromErr(err) + } + } + + if toRemove.Len() > 0 { + removeSlugs := make([]string, 0, toRemove.Len()) + for _, item := range toRemove.List() { + removeSlugs = append(removeSlugs, item.(string)) + } + _, _, err = client.Enterprise.RemoveMultipleAssignments(ctx, enterpriseSlug, teamSlug, removeSlugs) + if err != nil { + return diag.FromErr(err) + } + } + } + + return nil +} + +func resourceGithubEnterpriseTeamOrganizationsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + enterpriseSlug, teamSlug, err := parseEnterpriseTeamOrganizationsID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + orgs, err := listAllEnterpriseTeamOrganizations(ctx, client, enterpriseSlug, teamSlug) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) + } + + removeSlugs := make([]string, 0, len(orgs)) + for _, org := range orgs { + if org.Login != nil && *org.Login != "" { + removeSlugs = append(removeSlugs, *org.Login) + } + } + + if len(removeSlugs) > 0 { + _, resp, err := client.Enterprise.RemoveMultipleAssignments(ctx, enterpriseSlug, teamSlug, removeSlugs) + if err != nil { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil + } + return diag.FromErr(err) + } + } + + return 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..ef8ff0f9d0 --- /dev/null +++ b/github/resource_github_enterprise_team_test.go @@ -0,0 +1,276 @@ +package github + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubEnterpriseTeam(t *testing.T) { + t.Run("creates and updates resource without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + description = "team for acceptance testing" + organization_selection_type = "disabled" + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_team.test", tfjsonpath.New("slug"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_enterprise_team.test", tfjsonpath.New("team_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_enterprise_team.test", tfjsonpath.New("organization_selection_type"), knownvalue.StringExact("disabled")), + }, + }, + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + description = "updated description" + organization_selection_type = "selected" + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_team.test", tfjsonpath.New("description"), knownvalue.StringExact("updated description")), + statecheck.ExpectKnownValue("github_enterprise_team.test", tfjsonpath.New("organization_selection_type"), knownvalue.StringExact("selected")), + }, + }, + }, + }) + }) + + t.Run("imports resource without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + description = "team for import testing" + organization_selection_type = "disabled" + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID), + }, + { + ResourceName: "github_enterprise_team.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"group_id"}, + ImportStateIdPrefix: fmt.Sprintf(`%s/`, testAccConf.enterpriseSlug), + }, + }, + }) + }) +} + +func TestAccGithubEnterpriseTeamOrganizations(t *testing.T) { + orgSlug := os.Getenv("ENTERPRISE_TEST_ORGANIZATION") + if orgSlug == "" { + t.Skip("ENTERPRISE_TEST_ORGANIZATION not set") + } + + t.Run("assigns organizations to team without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + organization_selection_type = "selected" + } + + resource "github_enterprise_team_organizations" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + organization_slugs = [%q] + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID, orgSlug), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_team_organizations.test", tfjsonpath.New("organization_slugs"), knownvalue.SetSizeExact(1)), + statecheck.ExpectKnownValue("github_enterprise_team_organizations.test", tfjsonpath.New("organization_slugs"), knownvalue.SetPartial([]knownvalue.Check{knownvalue.StringExact(orgSlug)})), + }, + }, + }, + }) + }) + + t.Run("imports resource without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + organization_selection_type = "selected" + } + + resource "github_enterprise_team_organizations" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + organization_slugs = [%q] + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID, orgSlug), + }, + { + ResourceName: "github_enterprise_team_organizations.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("errors on empty organizations", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + organization_selection_type = "selected" + } + + resource "github_enterprise_team_organizations" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + organization_slugs = [] + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID), + ExpectError: regexp.MustCompile(`Attribute organization_slugs requires 1 item minimum`), + }, + }, + }) + }) +} + +func TestAccGithubEnterpriseTeamMembership(t *testing.T) { + username := os.Getenv("ENTERPRISE_TEST_USER") + if username == "" { + t.Skip("ENTERPRISE_TEST_USER not set") + } + + t.Run("adds member to team without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + } + + resource "github_enterprise_team_membership" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + username = %q + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID, username), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_enterprise_team_membership.test", tfjsonpath.New("username"), knownvalue.StringExact(username)), + }, + }, + }, + }) + }) + + t.Run("imports resource without error", func(t *testing.T) { + randomID := acctest.RandString(5) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + resource "github_enterprise_team" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s%s" + } + + resource "github_enterprise_team_membership" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + team_slug = github_enterprise_team.test.slug + username = %q + } + `, testAccConf.enterpriseSlug, testResourcePrefix, randomID, username), + }, + { + ResourceName: "github_enterprise_team_membership.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) +} diff --git a/github/util_enterprise_teams.go b/github/util_enterprise_teams.go new file mode 100644 index 0000000000..d0727ea71e --- /dev/null +++ b/github/util_enterprise_teams.go @@ -0,0 +1,105 @@ +package github + +import ( + "context" + "fmt" + "strings" + + "github.com/google/go-github/v83/github" +) + +// buildEnterpriseTeamMembershipID creates an ID for enterprise team membership resources. +// Uses "/" as separator because team slugs contain ":" (e.g., "ent:team-name"). +// Note: GitHub slugs only allow alphanumeric characters, hyphens, and colons - never "/". +func buildEnterpriseTeamMembershipID(enterpriseSlug, teamSlug, username string) string { + return fmt.Sprintf("%s/%s/%s", enterpriseSlug, teamSlug, username) +} + +// parseEnterpriseTeamMembershipID parses the ID for enterprise team membership resources. +func parseEnterpriseTeamMembershipID(id string) (enterpriseSlug, teamSlug, username string, err error) { + parts := strings.SplitN(id, "/", 3) + if len(parts) != 3 { + return "", "", "", fmt.Errorf("unexpected ID format (%q); expected enterprise_slug/team_slug/username", id) + } + return parts[0], parts[1], parts[2], nil +} + +// buildEnterpriseTeamOrganizationsID creates an ID for enterprise team organizations resources. +// Uses "/" as separator because team slugs contain ":" (e.g., "ent:team-name"). +// Note: GitHub slugs only allow alphanumeric characters, hyphens, and colons - never "/". +func buildEnterpriseTeamOrganizationsID(enterpriseSlug, teamSlug string) string { + return fmt.Sprintf("%s/%s", enterpriseSlug, teamSlug) +} + +// parseEnterpriseTeamOrganizationsID parses the ID for enterprise team organizations resources. +func parseEnterpriseTeamOrganizationsID(id string) (enterpriseSlug, teamSlug string, err error) { + parts := strings.SplitN(id, "/", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("unexpected ID format (%q); expected enterprise_slug/team_slug", id) + } + return parts[0], parts[1], nil +} + +// findEnterpriseTeamByID lists all enterprise teams and returns the one matching the given ID. +// This is needed because the API doesn't provide a direct lookup by numeric ID. +func findEnterpriseTeamByID(ctx context.Context, client *github.Client, enterpriseSlug string, id int64) (*github.EnterpriseTeam, error) { + opt := &github.ListOptions{PerPage: maxPerPage} + + for { + teams, resp, err := client.Enterprise.ListTeams(ctx, enterpriseSlug, opt) + if err != nil { + return nil, err + } + for _, team := range teams { + if team.ID == id { + return team, nil + } + } + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return nil, nil +} + +// listAllEnterpriseTeamOrganizations returns all organizations assigned to an enterprise team with pagination handled. +func listAllEnterpriseTeamOrganizations(ctx context.Context, client *github.Client, enterpriseSlug, enterpriseTeam string) ([]*github.Organization, error) { + var all []*github.Organization + opt := &github.ListOptions{PerPage: maxPerPage} + + for { + orgs, resp, err := client.Enterprise.ListAssignments(ctx, enterpriseSlug, enterpriseTeam, opt) + if err != nil { + return nil, err + } + all = append(all, orgs...) + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return all, nil +} + +// listAllEnterpriseTeams returns all enterprise teams with pagination handled. +func listAllEnterpriseTeams(ctx context.Context, client *github.Client, enterpriseSlug string) ([]*github.EnterpriseTeam, error) { + var all []*github.EnterpriseTeam + opt := &github.ListOptions{PerPage: maxPerPage} + + for { + teams, resp, err := client.Enterprise.ListTeams(ctx, enterpriseSlug, opt) + if err != nil { + return nil, err + } + all = append(all, teams...) + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + return all, nil +} diff --git a/github/util_enterprise_teams_test.go b/github/util_enterprise_teams_test.go new file mode 100644 index 0000000000..0229a51e63 --- /dev/null +++ b/github/util_enterprise_teams_test.go @@ -0,0 +1,140 @@ +package github + +import ( + "testing" +) + +func TestBuildEnterpriseTeamMembershipID(t *testing.T) { + t.Run("builds correct ID format", func(t *testing.T) { + got := buildEnterpriseTeamMembershipID("my-enterprise", "ent:my-team", "testuser") + want := "my-enterprise/ent:my-team/testuser" + if got != want { + t.Fatalf("buildEnterpriseTeamMembershipID() = %q, want %q", got, want) + } + }) + + t.Run("handles empty strings", func(t *testing.T) { + got := buildEnterpriseTeamMembershipID("", "", "") + want := "//" + if got != want { + t.Fatalf("buildEnterpriseTeamMembershipID() = %q, want %q", got, want) + } + }) +} + +func TestParseEnterpriseTeamMembershipID(t *testing.T) { + t.Run("parses valid ID", func(t *testing.T) { + enterprise, teamSlug, username, err := parseEnterpriseTeamMembershipID("my-enterprise/ent:my-team/testuser") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if enterprise != "my-enterprise" { + t.Fatalf("enterprise = %q, want %q", enterprise, "my-enterprise") + } + if teamSlug != "ent:my-team" { + t.Fatalf("teamSlug = %q, want %q", teamSlug, "ent:my-team") + } + if username != "testuser" { + t.Fatalf("username = %q, want %q", username, "testuser") + } + }) + + t.Run("parses ID with slashes in username", func(t *testing.T) { + enterprise, teamSlug, username, err := parseEnterpriseTeamMembershipID("ent/team/user/extra") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if enterprise != "ent" { + t.Fatalf("enterprise = %q, want %q", enterprise, "ent") + } + if teamSlug != "team" { + t.Fatalf("teamSlug = %q, want %q", teamSlug, "team") + } + if username != "user/extra" { + t.Fatalf("username = %q, want %q", username, "user/extra") + } + }) + + t.Run("returns error for invalid format", func(t *testing.T) { + _, _, _, err := parseEnterpriseTeamMembershipID("only-one-part") + if err == nil { + t.Fatal("expected error for invalid ID format, got nil") + } + }) + + t.Run("returns error for empty string", func(t *testing.T) { + _, _, _, err := parseEnterpriseTeamMembershipID("") + if err == nil { + t.Fatal("expected error for empty ID, got nil") + } + }) + + t.Run("roundtrips with build function", func(t *testing.T) { + id := buildEnterpriseTeamMembershipID("enterprise", "ent:team-slug", "user123") + enterprise, teamSlug, username, err := parseEnterpriseTeamMembershipID(id) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if enterprise != "enterprise" || teamSlug != "ent:team-slug" || username != "user123" { + t.Fatalf("roundtrip failed: got (%q, %q, %q)", enterprise, teamSlug, username) + } + }) +} + +func TestBuildEnterpriseTeamOrganizationsID(t *testing.T) { + t.Run("builds correct ID format", func(t *testing.T) { + got := buildEnterpriseTeamOrganizationsID("my-enterprise", "ent:my-team") + want := "my-enterprise/ent:my-team" + if got != want { + t.Fatalf("buildEnterpriseTeamOrganizationsID() = %q, want %q", got, want) + } + }) + + t.Run("handles empty strings", func(t *testing.T) { + got := buildEnterpriseTeamOrganizationsID("", "") + want := "/" + if got != want { + t.Fatalf("buildEnterpriseTeamOrganizationsID() = %q, want %q", got, want) + } + }) +} + +func TestParseEnterpriseTeamOrganizationsID(t *testing.T) { + t.Run("parses valid ID", func(t *testing.T) { + enterprise, teamSlug, err := parseEnterpriseTeamOrganizationsID("my-enterprise/ent:my-team") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if enterprise != "my-enterprise" { + t.Fatalf("enterprise = %q, want %q", enterprise, "my-enterprise") + } + if teamSlug != "ent:my-team" { + t.Fatalf("teamSlug = %q, want %q", teamSlug, "ent:my-team") + } + }) + + t.Run("returns error for invalid format", func(t *testing.T) { + _, _, err := parseEnterpriseTeamOrganizationsID("no-slash-here") + if err == nil { + t.Fatal("expected error for invalid ID format, got nil") + } + }) + + t.Run("returns error for empty string", func(t *testing.T) { + _, _, err := parseEnterpriseTeamOrganizationsID("") + if err == nil { + t.Fatal("expected error for empty ID, got nil") + } + }) + + t.Run("roundtrips with build function", func(t *testing.T) { + id := buildEnterpriseTeamOrganizationsID("enterprise", "ent:team-slug") + enterprise, teamSlug, err := parseEnterpriseTeamOrganizationsID(id) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if enterprise != "enterprise" || teamSlug != "ent:team-slug" { + t.Fatalf("roundtrip failed: got (%q, %q)", enterprise, teamSlug) + } + }) +} diff --git a/website/docs/d/enterprise_team.html.markdown b/website/docs/d/enterprise_team.html.markdown new file mode 100644 index 0000000000..edf72752c1 --- /dev/null +++ b/website/docs/d/enterprise_team.html.markdown @@ -0,0 +1,49 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_team" +description: |- + Gets 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. Exactly one of `slug` or `team_id` must be specified. +* `team_id` - (Optional) The numeric ID of the enterprise team. Exactly one of `slug` or `team_id` must be specified. + +## 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..ce3730452a --- /dev/null +++ b/website/docs/d/enterprise_team_membership.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_team_membership" +description: |- + Checks 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" + team_slug = "ent:platform" + username = "octocat" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `team_slug` - (Required) The slug of the enterprise team. +* `username` - (Required) The GitHub username. + +## Attributes Reference + +The following additional attributes are exported: + +* `user_id` - The ID of the user. 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..a1989eb7e8 --- /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: |- + Gets 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" + team_slug = "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. +* `team_slug` - (Required) The slug 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..89ac8a56a1 --- /dev/null +++ b/website/docs/d/enterprise_teams.html.markdown @@ -0,0 +1,45 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_teams" +description: |- + Lists 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..768add8f5b --- /dev/null +++ b/website/docs/r/enterprise_team.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_team" +description: |- + Creates 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..881f1675da --- /dev/null +++ b/website/docs/r/enterprise_team_membership.html.markdown @@ -0,0 +1,54 @@ +--- +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 + team_slug = github_enterprise_team.team.slug + username = "octocat" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `enterprise_slug` - (Required) The slug of the enterprise. +* `team_slug` - (Optional) The slug of the enterprise team. Exactly one of `team_slug` or `team_id` must be specified. +* `team_id` - (Optional) The ID of the enterprise team. Exactly one of `team_slug` or `team_id` must be specified. +* `username` - (Required) The GitHub username to manage. + +## Attributes Reference + +The following additional attributes are exported: + +* `user_id` - The ID of the user. + +## 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..967e9d7c7a --- /dev/null +++ b/website/docs/r/enterprise_team_organizations.html.markdown @@ -0,0 +1,58 @@ +--- +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 + team_slug = 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. +* `team_slug` - (Optional) The slug of the enterprise team. Exactly one of `team_slug` or `team_id` must be specified. +* `team_id` - (Optional) The ID of the enterprise team. Exactly one of `team_slug` or `team_id` must be specified. +* `organization_slugs` - (Required) Set of organization slugs to assign the team to (minimum 1). + +## Attributes Reference + +This resource exports no additional attributes. + +## Import + +This resource can be imported using: + +``` +$ terraform import github_enterprise_team_organizations.assignments enterprise-slug/ent:platform +``` diff --git a/website/github.erb b/website/github.erb index 997536b42f..4fccb2b526 100644 --- a/website/github.erb +++ b/website/github.erb @@ -100,6 +100,18 @@
  • github_enterprise
  • +
  • + github_enterprise_team +
  • +
  • + github_enterprise_team_membership +
  • +
  • + github_enterprise_team_organizations +
  • +
  • + github_enterprise_teams +
  • github_external_groups
  • @@ -313,6 +325,15 @@
  • github_enterprise_security_analysis_settings
  • +
  • + github_enterprise_team +
  • +
  • + github_enterprise_team_membership +
  • +
  • + github_enterprise_team_organizations +
  • github_issue