diff --git a/github/provider.go b/github/provider.go index 003844cbc3..69499fc6e1 100644 --- a/github/provider.go +++ b/github/provider.go @@ -110,6 +110,7 @@ func Provider() terraform.ResourceProvider { "github_membership": resourceGithubMembership(), "github_organization_block": resourceOrganizationBlock(), "github_organization_project": resourceGithubOrganizationProject(), + "github_organization_security_manager": resourceGithubOrganizationSecurityManager(), "github_organization_settings": resourceGithubOrganizationSettings(), "github_organization_webhook": resourceGithubOrganizationWebhook(), "github_project_card": resourceGithubProjectCard(), diff --git a/github/resource_github_organization_security_manager.go b/github/resource_github_organization_security_manager.go new file mode 100644 index 0000000000..71b1b89a65 --- /dev/null +++ b/github/resource_github_organization_security_manager.go @@ -0,0 +1,151 @@ +package github + +import ( + "context" + "log" + "net/http" + "strconv" + + "github.com/google/go-github/v48/github" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourceGithubOrganizationSecurityManager() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubOrganizationSecurityManagerCreate, + Read: resourceGithubOrganizationSecurityManagerRead, + Update: resourceGithubOrganizationSecurityManagerUpdate, + Delete: resourceGithubOrganizationSecurityManagerDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "team_slug": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceGithubOrganizationSecurityManagerCreate(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + orgName := meta.(*Owner).name + teamSlug := d.Get("team_slug").(string) + + client := meta.(*Owner).v3client + ctx := context.Background() + + team, _, err := client.Teams.GetTeamBySlug(ctx, orgName, teamSlug) + if err != nil { + log.Printf("[INFO] Team %s/%s was not found in GitHub", orgName, teamSlug) + return err + } + + _, err = client.Organizations.AddSecurityManagerTeam(ctx, orgName, teamSlug) + if err != nil { + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusConflict { + log.Printf("[WARN] Organization %s has reached the maximum number of security manager teams", orgName) + return nil + } + } + return err + } + + d.SetId(strconv.FormatInt(team.GetID(), 10)) + + return resourceGithubOrganizationSecurityManagerRead(d, meta) +} + +func resourceGithubOrganizationSecurityManagerRead(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + orgName := meta.(*Owner).name + teamId, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + // There is no endpoint for getting a single security manager team, so get the list and filter. + // There is a maximum number of security manager teams (currently 10), so this should be fine. + teams, _, err := client.Organizations.ListSecurityManagerTeams(ctx, orgName) + if err != nil { + return err + } + + var team *github.Team + for _, t := range teams { + if t.GetID() == teamId { + team = t + break + } + } + + if team == nil { + log.Printf("[WARN] Removing organization security manager team %s from state because it no longer exists in GitHub", d.Id()) + d.SetId("") + return nil + } + + d.Set("team_slug", team.GetSlug()) + + return nil +} + +func resourceGithubOrganizationSecurityManagerUpdate(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + orgId := meta.(*Owner).id + orgName := meta.(*Owner).name + teamId, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + team, _, err := client.Teams.GetTeamByID(ctx, orgId, teamId) + if err != nil { + return err + } + + // Adding the same team is a no-op. + _, err = client.Organizations.AddSecurityManagerTeam(ctx, orgName, team.GetSlug()) + if err != nil { + return err + } + + return resourceGithubOrganizationSecurityManagerRead(d, meta) +} + +func resourceGithubOrganizationSecurityManagerDelete(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + orgName := meta.(*Owner).name + teamSlug := d.Get("team_slug").(string) + + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + _, err = client.Organizations.RemoveSecurityManagerTeam(ctx, orgName, teamSlug) + return err +} diff --git a/github/resource_github_organization_security_manager_test.go b/github/resource_github_organization_security_manager_test.go new file mode 100644 index 0000000000..70415c4a93 --- /dev/null +++ b/github/resource_github_organization_security_manager_test.go @@ -0,0 +1,172 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccGithubOrganizationSecurityManagers(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + t.Run("adds team as security manager", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_team" "test" { + name = "tf-acc-%s" + } + + resource "github_organization_security_manager" "test" { + team_slug = github_team.test.slug + } + `, randomID) + + 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: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"), + resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"), + resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-%s", randomID)), + ), + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("handles team name changes", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_team" "test" { + name = "tf-acc-%s" + } + + resource "github_organization_security_manager" "test" { + team_slug = github_team.test.slug + } + `, randomID) + + configUpdated := fmt.Sprintf(` + resource "github_team" "test" { + name = "tf-acc-updated-%s" + } + + resource "github_organization_security_manager" "test" { + team_slug = github_team.test.slug + } + `, randomID) + + 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: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"), + resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"), + resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-%s", randomID)), + ), + }, + { + Config: configUpdated, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"), + resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"), + resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-updated-%s", randomID)), + ), + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("handles team name changes", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_team" "test" { + name = "tf-acc-%s" + } + + resource "github_organization_security_manager" "test" { + team_slug = github_team.test.slug + } + `, randomID) + + configUpdated := fmt.Sprintf(` + resource "github_team" "test" { + name = "tf-acc-updated-%s" + } + + resource "github_organization_security_manager" "test" { + team_slug = github_team.test.slug + } + `, randomID) + + 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: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"), + resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"), + resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-%s", randomID)), + ), + }, + { + Config: configUpdated, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("github_team.test", "ID", "github_organization_security_manager.test", "ID"), + resource.TestCheckResourceAttrPair("github_team.test", "slug", "github_organization_security_manager.test", "team_slug"), + resource.TestCheckResourceAttr("github_organization_security_manager.test", "team_slug", fmt.Sprintf("tf-acc-updated-%s", randomID)), + ), + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) +} diff --git a/website/docs/r/organization_security_manager.html.markdown b/website/docs/r/organization_security_manager.html.markdown new file mode 100644 index 0000000000..246dbb7729 --- /dev/null +++ b/website/docs/r/organization_security_manager.html.markdown @@ -0,0 +1,35 @@ +--- +layout: "github" +page_title: "GitHub: github_organization_security_manager" +description: |- + Manages the Security manager teams for a GitHub Organization. +--- + +# github_organization_security_manager + +## Example Usage + +```hcl +resource "github_team" "some_team" { + name = "SomeTeam" + description = "Some cool team" +} + +resource "github_organization_security_manager" "some_team" { + team_slug = github_team.some_team.slug +} +``` + +## Argument Reference + +The following arguments are supported: + +* `team_slug` - (Required) The slug of the team to manage. + +## Import + +GitHub Security Manager Teams can be imported using the GitHub team ID e.g. + +``` +$ terraform import github_organization_security_manager.core 1234567 +``` diff --git a/website/github.erb b/website/github.erb index a00c4cc01e..903915096b 100644 --- a/website/github.erb +++ b/website/github.erb @@ -154,6 +154,9 @@
  • github_organization_project
  • +
  • + github_organization_security_manager +
  • github_organization_settings