diff --git a/examples/repository_collaborator/README.md b/examples/repository_collaborator/README.md index 2bff32ee3a..49ff4d3d0e 100644 --- a/examples/repository_collaborator/README.md +++ b/examples/repository_collaborator/README.md @@ -7,7 +7,7 @@ This example will also create a repository in the specified `owner` organization Alternatively, you may use variables passed via command line: ```console -export GITHUB_ORG= +export GITHUB_ORGANIZATION= export GITHUB_TOKEN= export COLLABORATOR_USERNAME= export COLLABORATOR_PERMISSION= @@ -15,7 +15,7 @@ export COLLABORATOR_PERMISSION= ```console terraform apply \ - -var "organization=${GITHUB_ORG}" \ + -var "organization=${GITHUB_ORGANIZATION}" \ -var "github_token=${GITHUB_TOKEN}" \ -var "username=${COLLABORATOR_USERNAME}" \ -var "permission=${COLLABORATOR_PERMISSION}" diff --git a/examples/repository_collaborator/providers.tf b/examples/repository_collaborator/providers.tf index 2ba0931912..6ec2a6fd52 100644 --- a/examples/repository_collaborator/providers.tf +++ b/examples/repository_collaborator/providers.tf @@ -1,5 +1,4 @@ provider "github" { - version = "2.8.0" organization = var.organization token = var.github_token } diff --git a/github/resource_github_repository_collaborator.go b/github/resource_github_repository_collaborator.go index abb59f917b..901d6996c8 100644 --- a/github/resource_github_repository_collaborator.go +++ b/github/resource_github_repository_collaborator.go @@ -15,6 +15,7 @@ func resourceGithubRepositoryCollaborator() *schema.Resource { return &schema.Resource{ Create: resourceGithubRepositoryCollaboratorCreate, Read: resourceGithubRepositoryCollaboratorRead, + Update: resourceGithubRepositoryCollaboratorUpdate, Delete: resourceGithubRepositoryCollaboratorDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, @@ -39,6 +40,19 @@ func resourceGithubRepositoryCollaborator() *schema.Resource { ForceNew: true, Default: "push", ValidateFunc: validateValueFunc([]string{"pull", "triage", "push", "maintain", "admin"}), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if d.Get("permission_diff_suppression").(bool) { + if new == "triage" || new == "maintain" { + return true + } + } + return false + }, + }, + "permission_diff_suppression": { + Type: schema.TypeBool, + Optional: true, + Default: false, }, "invitation_id": { Type: schema.TypeString, @@ -158,6 +172,10 @@ func resourceGithubRepositoryCollaboratorRead(d *schema.ResourceData, meta inter return nil } +func resourceGithubRepositoryCollaboratorUpdate(d *schema.ResourceData, meta interface{}) error { + return resourceGithubRepositoryCollaboratorRead(d, meta) +} + func resourceGithubRepositoryCollaboratorDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*Owner).v3client diff --git a/github/resource_github_repository_collaborator_test.go b/github/resource_github_repository_collaborator_test.go index f6bec2b25f..64cc8de347 100644 --- a/github/resource_github_repository_collaborator_test.go +++ b/github/resource_github_repository_collaborator_test.go @@ -1,397 +1,65 @@ package github import ( - "context" - "errors" "fmt" - "regexp" - "strings" "testing" - "github.com/google/go-github/v32/github" "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" ) -// TestAccGithubRepositoryCollaborator_basic adds a collaborator -// with permissions supported by an organization-owned repository -// i.e. admin, triage, push, or pull -func TestAccGithubRepositoryCollaborator_basic(t *testing.T) { - if testCollaborator == "" { - t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") - } +func TestAccGithubRepositoryCollaborator(t *testing.T) { - if err := testAccCheckOrganization(); err != nil { - t.Skipf("Skipping because %s", err.Error()) - } + t.Skip("update below to unskip this test run") - rn := "github_repository_collaborator.test_repo_collaborator" - repoName := fmt.Sprintf("tf-acc-test-collab-%s", acctest.RandString(5)) + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - permissionAdmin := "admin" - permissionTriage := "triage" - permissionPush := "push" - permissionPull := "pull" + t.Run("creates invitations without error", func(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckGithubRepositoryCollaboratorDestroy, - Steps: []resource.TestStep{ - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator, permissionPull), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorExists(rn), - testAccCheckGithubRepositoryCollaboratorPermission(rn, permissionPull), - resource.TestCheckResourceAttr(rn, "permission", permissionPull), - resource.TestMatchResourceAttr(rn, "invitation_id", regexp.MustCompile(`^[0-9]+$`)), - ), - }, - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator, permissionPush), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorExists(rn), - testAccCheckGithubRepositoryCollaboratorPermission(rn, permissionPush), - resource.TestCheckResourceAttr(rn, "permission", permissionPush), - resource.TestMatchResourceAttr(rn, "invitation_id", regexp.MustCompile(`^[0-9]+$`)), - ), - }, - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator, permissionAdmin), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorExists(rn), - testAccCheckGithubRepositoryCollaboratorPermission(rn, permissionAdmin), - resource.TestCheckResourceAttr(rn, "permission", permissionAdmin), - resource.TestMatchResourceAttr(rn, "invitation_id", regexp.MustCompile(`^[0-9]+$`)), - ), - }, - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator, permissionTriage), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorExists(rn), - testAccCheckGithubRepositoryCollaboratorPermission(rn, permissionTriage), - resource.TestCheckResourceAttr(rn, "permission", permissionTriage), - resource.TestMatchResourceAttr(rn, "invitation_id", regexp.MustCompile(`^[0-9]+$`)), - ), - }, - { - ResourceName: rn, - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -// TestAccGithubRepositoryCollaborator_basic_personal -// adds a collaborator with permissions supported by -// a personal repository i.e. push -func TestAccGithubRepositoryCollaborator_basic_personal(t *testing.T) { - if testCollaborator == "" { - t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") - } - - rn := "github_repository_collaborator.test_repo_collaborator" - repoName := fmt.Sprintf("tf-acc-test-collab-%s", acctest.RandString(5)) - - permissionPush := "push" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckGithubRepositoryCollaboratorDestroy, - Steps: []resource.TestStep{ - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator, permissionPush), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorExists(rn), - testAccCheckGithubRepositoryCollaboratorPermission(rn, permissionPush), - resource.TestCheckResourceAttr(rn, "permission", permissionPush), - resource.TestMatchResourceAttr(rn, "invitation_id", regexp.MustCompile(`^[0-9]+$`)), - ), - }, - { - ResourceName: rn, - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -// TestAccGithubRepositoryCollaborator_caseInsensitive -// adds a collaborator with maintain permissions; -// only supported by an organization-owned repository -func TestAccGithubRepositoryCollaborator_caseInsensitive(t *testing.T) { - if testCollaborator == "" { - t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") - } - - if err := testAccCheckOrganization(); err != nil { - t.Skipf("Skipping because %s.", err.Error()) - } - - rn := "github_repository_collaborator.test_repo_collaborator" - repoName := fmt.Sprintf("tf-acc-test-collab-%s", acctest.RandString(5)) - - var origInvitation github.RepositoryInvitation - var otherInvitation github.RepositoryInvitation - - expectedPermission := "maintain" - - otherCase := flipUsernameCase(testCollaborator) - - if testCollaborator == otherCase { - t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` has no letters to flip case") - } - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckGithubRepositoryCollaboratorDestroy, - Steps: []resource.TestStep{ - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator, expectedPermission), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorInvited(repoName, testCollaborator, &origInvitation), - ), - }, - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, otherCase, expectedPermission), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorInvited(repoName, otherCase, &otherInvitation), - resource.TestCheckResourceAttr(rn, "username", testCollaborator), - testAccGithubRepositoryCollaboratorTheSame(&origInvitation, &otherInvitation), - ), - }, - { - ResourceName: rn, - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -// TestAccGithubRepositoryCollaborator_caseInsensitive_personal -// adds a collaborator with push permissions; supported by both -// a personal and an organization-owned repository -func TestAccGithubRepositoryCollaborator_caseInsensitive_personal(t *testing.T) { - if testCollaborator == "" { - t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set") - } - - rn := "github_repository_collaborator.test_repo_collaborator" - repoName := fmt.Sprintf("tf-acc-test-collab-%s", acctest.RandString(5)) - - var origInvitation github.RepositoryInvitation - var otherInvitation github.RepositoryInvitation - - expectedPermission := "push" - - otherCase := flipUsernameCase(testCollaborator) - - if testCollaborator == otherCase { - t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` has no letters to flip case") - } - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckGithubRepositoryCollaboratorDestroy, - Steps: []resource.TestStep{ - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, testCollaborator, expectedPermission), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorInvited(repoName, testCollaborator, &origInvitation), - ), - }, - { - Config: testAccGithubRepositoryCollaboratorConfig(repoName, otherCase, expectedPermission), - Check: resource.ComposeTestCheckFunc( - testAccCheckGithubRepositoryCollaboratorInvited(repoName, otherCase, &otherInvitation), - resource.TestCheckResourceAttr(rn, "username", testCollaborator), - testAccGithubRepositoryCollaboratorTheSame(&origInvitation, &otherInvitation), - ), - }, - { - ResourceName: rn, - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -func testAccCheckGithubRepositoryCollaboratorDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*Owner).v3client - - for _, rs := range s.RootModule().Resources { - if rs.Type != "github_repository_collaborator" { - continue - } - - o := testAccProvider.Meta().(*Owner).name - r, u, err := parseTwoPartID(rs.Primary.ID, "repository", "username") - if err != nil { - return err - } - - isCollaborator, _, err := conn.Repositories.IsCollaborator(context.TODO(), o, r, u) - - if err != nil { - return err - } - - if isCollaborator { - return fmt.Errorf("Repository collaborator still exists") - } - - return nil - } - - return nil -} - -func testAccCheckGithubRepositoryCollaboratorExists(n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not Found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No membership ID is set") - } - - conn := testAccProvider.Meta().(*Owner).v3client - owner := testAccProvider.Meta().(*Owner).name - repoName, username, err := parseTwoPartID(rs.Primary.ID, "repository", "username") - if err != nil { - return err - } - - invitations, _, err := conn.Repositories.ListInvitations(context.TODO(), - owner, repoName, nil) - if err != nil { - return err - } - - hasInvitation := false - for _, i := range invitations { - if i.GetInvitee().GetLogin() == username { - hasInvitation = true - break - } - } - - if !hasInvitation { - return fmt.Errorf("Repository collaboration invitation does not exist") - } - - return nil - } -} - -func testAccCheckGithubRepositoryCollaboratorPermission(n, permission string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not Found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No membership ID is set") - } - - conn := testAccProvider.Meta().(*Owner).v3client - owner := testAccProvider.Meta().(*Owner).name - repoName, username, err := parseTwoPartID(rs.Primary.ID, "repository", "username") - if err != nil { - return err - } - - invitations, _, err := conn.Repositories.ListInvitations(context.TODO(), - owner, repoName, nil) - if err != nil { - return err - } - - for _, i := range invitations { - if i.GetInvitee().GetLogin() == username { - permName, err := getInvitationPermission(i) - - if err != nil { - return err - } - - if permName != permission { - return fmt.Errorf("Expected permission %s on repository collaborator, actual permission %s", permission, permName) - } - - return nil - } - } - - return fmt.Errorf("Repository collaborator did not appear in list of collaborators on repository") - } -} - -func testAccGithubRepositoryCollaboratorConfig(repoName, username, permission string) string { - return fmt.Sprintf(` -resource "github_repository" "test" { - name = "%s" -} - -resource "github_repository_collaborator" "test_repo_collaborator" { - repository = "${github_repository.test.name}" - username = "%s" - permission = "%s" -} -`, repoName, username, permission) -} - -func testAccCheckGithubRepositoryCollaboratorInvited(repoName, username string, invitation *github.RepositoryInvitation) resource.TestCheckFunc { - return func(s *terraform.State) error { - opt := &github.ListOptions{PerPage: maxPerPage} - - client := testAccProvider.Meta().(*Owner).v3client - owner := testAccProvider.Meta().(*Owner).name - - for { - invitations, resp, err := client.Repositories.ListInvitations(context.TODO(), owner, repoName, opt) - if err != nil { - return errors.New(err.Error()) - } - - if len(invitations) > 1 { - return fmt.Errorf("multiple invitations have been sent for repository %s", repoName) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-%s" + auto_init = true } - for _, i := range invitations { - if strings.EqualFold(i.GetInvitee().GetLogin(), username) { - invitation = i - return nil - } + resource "github_repository_collaborator" "test_repo_collaborator" { + repository = "${github_repository.test.name}" + username = "" + permission = "triage" } + `, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository_collaborator.test_repo_collaborator", "permission", + "triage", + ), + ) + + 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, + }, + }, + }) + } + + 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) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) - if resp.NextPage == 0 { - break - } - opt.Page = resp.NextPage - } - - return fmt.Errorf("no invitation found for %s", username) - } -} - -func testAccGithubRepositoryCollaboratorTheSame(orig, other *github.RepositoryInvitation) resource.TestCheckFunc { - return func(s *terraform.State) error { - if orig.GetID() != other.GetID() { - return errors.New("collaborators are different") - } - - return nil - } } diff --git a/website/docs/r/repository_collaborator.html.markdown b/website/docs/r/repository_collaborator.html.markdown index 482c66031a..f3f065d17e 100644 --- a/website/docs/r/repository_collaborator.html.markdown +++ b/website/docs/r/repository_collaborator.html.markdown @@ -46,6 +46,7 @@ The following arguments are supported: * `permission` - (Optional) The permission of the outside collaborator for the repository. Must be one of `pull`, `push`, `maintain`, `triage` or `admin` for organization-owned repositories. Must be `push` for personal repositories. Defaults to `push`. +* `permission_diff_suppression` - (Optional) Suppress plan diffs for `triage` and `maintain`. Defaults to `false`. ## Attribute Reference