Skip to content

Commit fabff68

Browse files
feat: add ability to downgrade membership when github_membership is destroyed (#1783)
* feat: add ability to downgrade membership on destroy * add docs * formatting * formatting * check membership status before downgrading * fix lint * fix lint * Update github/resource_github_membership.go Co-authored-by: Keegan Campbell <[email protected]> --------- Co-authored-by: Keegan Campbell <[email protected]>
1 parent 6f934ec commit fabff68

File tree

3 files changed

+105
-8
lines changed

3 files changed

+105
-8
lines changed

github/resource_github_membership.go

+40-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ func resourceGithubMembership() *schema.Resource {
3838
Type: schema.TypeString,
3939
Computed: true,
4040
},
41+
"downgrade_on_destroy": {
42+
Type: schema.TypeBool,
43+
Optional: true,
44+
Default: false,
45+
Description: "Instead of removing the member from the org, you can choose to downgrade their membership to 'member' when this resource is destroyed. This is useful when wanting to downgrade admins while keeping them in the organization",
46+
},
4147
},
4248
}
4349
}
@@ -126,8 +132,40 @@ func resourceGithubMembershipDelete(d *schema.ResourceData, meta interface{}) er
126132
orgName := meta.(*Owner).name
127133
ctx := context.WithValue(context.Background(), ctxId, d.Id())
128134

129-
_, err = client.Organizations.RemoveOrgMembership(ctx,
130-
d.Get("username").(string), orgName)
135+
username := d.Get("username").(string)
136+
downgradeOnDestroy := d.Get("downgrade_on_destroy").(bool)
137+
downgradeTo := "member"
138+
139+
if downgradeOnDestroy {
140+
log.Printf("[INFO] Downgrading '%s' membership for '%s' to '%s'", orgName, username, downgradeTo)
141+
142+
// Check to make sure this member still has access to the organization before downgrading.
143+
// If we don't do this, the member would just be re-added to the organization.
144+
var membership *github.Membership
145+
membership, _, err = client.Organizations.GetOrgMembership(ctx, username, orgName)
146+
if err != nil {
147+
if ghErr, ok := err.(*github.ErrorResponse); ok {
148+
if ghErr.Response.StatusCode == http.StatusNotFound {
149+
log.Printf("[INFO] Not downgrading '%s' membership for '%s' because they are not a member of the org anymore", orgName, username)
150+
return nil
151+
}
152+
}
153+
154+
return err
155+
}
156+
157+
if *membership.Role == downgradeTo {
158+
log.Printf("[INFO] Not downgrading '%s' membership for '%s' because they are already '%s'", orgName, username, downgradeTo)
159+
return nil
160+
}
161+
162+
_, _, err = client.Organizations.EditOrgMembership(ctx, username, orgName, &github.Membership{
163+
Role: github.String(downgradeTo),
164+
})
165+
} else {
166+
log.Printf("[INFO] Revoking '%s' membership for '%s'", orgName, username)
167+
_, err = client.Organizations.RemoveOrgMembership(ctx, username, orgName)
168+
}
131169

132170
return err
133171
}

github/resource_github_membership_test.go

+61-6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ func TestAccGithubMembership_basic(t *testing.T) {
4343
})
4444
}
4545

46+
func TestAccGithubMembership_downgrade(t *testing.T) {
47+
if testCollaborator == "" {
48+
t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set")
49+
}
50+
if err := testAccCheckOrganization(); err != nil {
51+
t.Skipf("Skipping because %s.", err.Error())
52+
}
53+
54+
var membership github.Membership
55+
rn := "github_membership.test_org_membership"
56+
57+
resource.ParallelTest(t, resource.TestCase{
58+
PreCheck: func() { testAccPreCheck(t) },
59+
Providers: testAccProviders,
60+
CheckDestroy: testAccCheckGithubMembershipDestroy,
61+
Steps: []resource.TestStep{
62+
{
63+
Config: testAccGithubMembershipConfigDowngradable(testCollaborator),
64+
Check: resource.ComposeTestCheckFunc(
65+
testAccCheckGithubMembershipExists(rn, &membership),
66+
testAccCheckGithubMembershipRoleState(rn, &membership),
67+
),
68+
},
69+
{
70+
ResourceName: rn,
71+
ImportState: true,
72+
},
73+
},
74+
})
75+
}
76+
4677
func TestAccGithubMembership_caseInsensitive(t *testing.T) {
4778
if testCollaborator == "" {
4879
t.Skip("Skipping because `GITHUB_TEST_COLLABORATOR` is not set")
@@ -95,22 +126,36 @@ func testAccCheckGithubMembershipDestroy(s *terraform.State) error {
95126
if rs.Type != "github_membership" {
96127
continue
97128
}
129+
98130
orgName, username, err := parseTwoPartID(rs.Primary.ID, "organization", "username")
99131
if err != nil {
100132
return err
101133
}
102134

135+
downgradedOnDestroy := rs.Primary.Attributes["downgrade_on_destroy"] == "true"
103136
membership, resp, err := conn.Organizations.GetOrgMembership(context.TODO(), username, orgName)
137+
responseIsSuccessful := err == nil && membership != nil && buildTwoPartID(orgName, username) == rs.Primary.ID
104138

105-
if err == nil {
106-
if membership != nil &&
107-
buildTwoPartID(orgName, username) == rs.Primary.ID {
108-
return fmt.Errorf("organization membership %q still exists", rs.Primary.ID)
139+
if downgradedOnDestroy {
140+
if !responseIsSuccessful {
141+
return fmt.Errorf("could not load organization membership for %q", rs.Primary.ID)
109142
}
110-
}
111-
if resp.StatusCode != 404 {
143+
144+
if *membership.Role != "member" {
145+
return fmt.Errorf("organization membership %q is not a member of the org or is not the 'member' role", rs.Primary.ID)
146+
}
147+
148+
// Now actually remove them from the org to clean up
149+
_, removeErr := conn.Organizations.RemoveOrgMembership(context.TODO(), username, orgName)
150+
if removeErr != nil {
151+
return fmt.Errorf("organization membership %q could not be removed during membership downgrade test case cleanup: %s", rs.Primary.ID, removeErr)
152+
}
153+
} else if responseIsSuccessful {
154+
return fmt.Errorf("organization membership %q still exists", rs.Primary.ID)
155+
} else if resp.StatusCode != 404 {
112156
return err
113157
}
158+
114159
return nil
115160
}
116161
return nil
@@ -184,6 +229,16 @@ func testAccGithubMembershipConfig(username string) string {
184229
`, username)
185230
}
186231

232+
func testAccGithubMembershipConfigDowngradable(username string) string {
233+
return fmt.Sprintf(`
234+
resource "github_membership" "test_org_membership" {
235+
username = "%s"
236+
role = "admin"
237+
downgrade_on_destroy = %t
238+
}
239+
`, username, true)
240+
}
241+
187242
func testAccGithubMembershipTheSame(orig, other *github.Membership) resource.TestCheckFunc {
188243
return func(s *terraform.State) error {
189244
if orig.GetURL() != other.GetURL() {

website/docs/r/membership.html.markdown

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ The following arguments are supported:
3030
* `username` - (Required) The user to add to the organization.
3131
* `role` - (Optional) The role of the user within the organization.
3232
Must be one of `member` or `admin`. Defaults to `member`.
33+
* `downgrade_on_destroy` - (Optional) Defaults to `false`. If set to true,
34+
when this resource is destroyed, the member will not be removed
35+
from the organization. Instead, the member's role will be
36+
downgraded to 'member'.
3337

3438

3539
## Import

0 commit comments

Comments
 (0)