Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion github/resource_github_membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"

"github.com/google/go-github/v85/github"
"github.com/hashicorp/terraform-plugin-log/tflog"
Expand Down Expand Up @@ -47,6 +48,11 @@ func resourceGithubMembership() *schema.Resource {
Default: false,
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",
},
"user_id": {
Type: schema.TypeString,
Computed: true,
Description: "The ID of the user, used to resolve the current username before membership updates in case the username changed (e.g. suspended EMU users).",
},
},
}
}
Expand Down Expand Up @@ -133,6 +139,13 @@ func resourceGithubMembershipRead(ctx context.Context, d *schema.ResourceData, m
return diag.FromErr(err)
}

user, _, err := client.Users.Get(ctx, username)
if err == nil {
if err = d.Set("user_id", strconv.FormatInt(user.GetID(), 10)); err != nil {
return diag.FromErr(err)
}
}

return nil
}

Expand All @@ -146,7 +159,7 @@ func resourceGithubMembershipDelete(ctx context.Context, d *schema.ResourceData,
orgName := meta.(*Owner).name
ctx = context.WithValue(ctx, ctxId, d.Id())

username := d.Get("username").(string)
username := resolveUsernameByID(ctx, client, d.Get("user_id").(string), d.Get("username").(string))
downgradeOnDestroy := d.Get("downgrade_on_destroy").(bool)
downgradeTo := "member"

Expand Down
16 changes: 15 additions & 1 deletion github/resource_github_repository_collaborator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log"
"net/http"
"strconv"
"strings"

"github.com/google/go-github/v85/github"
Expand Down Expand Up @@ -63,6 +64,11 @@ func resourceGithubRepositoryCollaborator() *schema.Resource {
Computed: true,
Description: "ID of the invitation to be used in 'github_user_invitation_accepter'",
},
"user_id": {
Type: schema.TypeString,
Computed: true,
Description: "The ID of the user, used to resolve the current username before collaborator updates in case the username changed (e.g. suspended EMU users).",
},
},
}
}
Expand Down Expand Up @@ -162,6 +168,9 @@ func resourceGithubRepositoryCollaboratorRead(d *schema.ResourceData, meta any)
if err = d.Set("permission", getPermission(c.GetRoleName())); err != nil {
return err
}
if err = d.Set("user_id", strconv.FormatInt(c.GetID(), 10)); err != nil {
return err
}
return nil
}
}
Expand All @@ -187,7 +196,12 @@ func resourceGithubRepositoryCollaboratorUpdate(d *schema.ResourceData, meta any
func resourceGithubRepositoryCollaboratorDelete(d *schema.ResourceData, meta any) error {
client := meta.(*Owner).v3client

username := d.Get("username").(string)
username := resolveUsernameByID(
context.WithValue(context.Background(), ctxId, d.Id()),
client,
d.Get("user_id").(string),
d.Get("username").(string),
)
repoName := d.Get("repository").(string)

owner, repoNameWithoutOwner := parseRepoName(repoName, meta.(*Owner).name)
Expand Down
35 changes: 30 additions & 5 deletions github/resource_github_team_members.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ func resourceGithubTeamMembers() *schema.Resource {
},
},
},
"user_ids": {
Type: schema.TypeMap,
Computed: true,
Description: "Map of usernames to their numeric user IDs, used to resolve current usernames before membership updates in case the username changed (e.g. suspended EMU users).",
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}
Expand Down Expand Up @@ -107,6 +113,11 @@ func resourceGithubTeamMembersUpdate(ctx context.Context, d *schema.ResourceData
return diag.FromErr(err)
}

storedUserIds := make(map[string]string)
for k, v := range d.Get("user_ids").(map[string]any) {
storedUserIds[k] = v.(string)
}

o, n := d.GetChange("members")
vals := make(map[string]*MemberChange)
for _, raw := range o.(*schema.Set).List() {
Expand Down Expand Up @@ -143,9 +154,10 @@ func resourceGithubTeamMembersUpdate(ctx context.Context, d *schema.ResourceData
}

if del {
log.Printf("[DEBUG] Deleting team membership: %s/%s", teamIdString, username)
currentUsername := resolveUsernameByID(ctx, client, storedUserIds[username], username)
log.Printf("[DEBUG] Deleting team membership: %s/%s", teamIdString, currentUsername)

_, err = client.Teams.RemoveTeamMembershipByID(ctx, orgId, teamId, username)
_, err = client.Teams.RemoveTeamMembershipByID(ctx, orgId, teamId, currentUsername)
if err != nil {
return diag.FromErr(err)
}
Expand Down Expand Up @@ -204,7 +216,8 @@ func resourceGithubTeamMembersRead(ctx context.Context, d *schema.ResourceData,
Members struct {
Edges []struct {
Node struct {
Login string
Login string
DatabaseId int64
}
Role string
}
Expand All @@ -224,6 +237,7 @@ func resourceGithubTeamMembersRead(ctx context.Context, d *schema.ResourceData,
}

var teamMembersAndMaintainers []any
userIds := make(map[string]string)
for {
if err := client.Query(ctx, &q, variables); err != nil {
return diag.FromErr(err)
Expand All @@ -235,6 +249,7 @@ func resourceGithubTeamMembersRead(ctx context.Context, d *schema.ResourceData,
"username": member.Node.Login,
"role": strings.ToLower(member.Role),
})
userIds[member.Node.Login] = strconv.FormatInt(member.Node.DatabaseId, 10)
}
if !q.Organization.Team.Members.PageInfo.HasNextPage {
break
Expand All @@ -246,6 +261,10 @@ func resourceGithubTeamMembersRead(ctx context.Context, d *schema.ResourceData,
return diag.FromErr(err)
}

if err := d.Set("user_ids", userIds); err != nil {
return diag.FromErr(err)
}

return nil
}

Expand All @@ -260,15 +279,21 @@ func resourceGithubTeamMembersDelete(ctx context.Context, d *schema.ResourceData
return diag.FromErr(err)
}

storedUserIds := make(map[string]string)
for k, v := range d.Get("user_ids").(map[string]any) {
storedUserIds[k] = v.(string)
}

members := d.Get("members").(*schema.Set)

for _, member := range members.List() {
mem := member.(map[string]any)
username := mem["username"].(string)
currentUsername := resolveUsernameByID(ctx, client, storedUserIds[username], username)

log.Printf("[DEBUG] Deleting team membership: %s/%s", teamIdString, username)
log.Printf("[DEBUG] Deleting team membership: %s/%s", teamIdString, currentUsername)

_, err = client.Teams.RemoveTeamMembershipByID(ctx, orgId, teamId, username)
_, err = client.Teams.RemoveTeamMembershipByID(ctx, orgId, teamId, currentUsername)
if err != nil {
return diag.FromErr(err)
}
Expand Down
15 changes: 14 additions & 1 deletion github/resource_github_team_membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ func resourceGithubTeamMembership() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"user_id": {
Type: schema.TypeString,
Computed: true,
Description: "The ID of the user, used to resolve the current username before membership updates in case the username changed (e.g. suspended EMU users).",
},
},
}
}
Expand Down Expand Up @@ -150,6 +155,13 @@ func resourceGithubTeamMembershipRead(ctx context.Context, d *schema.ResourceDat
return diag.FromErr(err)
}

user, _, err := client.Users.Get(ctx, username)
if err == nil {
if err = d.Set("user_id", strconv.FormatInt(user.GetID(), 10)); err != nil {
return diag.FromErr(err)
}
}

return nil
}

Expand All @@ -163,7 +175,8 @@ func resourceGithubTeamMembershipDelete(ctx context.Context, d *schema.ResourceD
if err != nil {
return diag.FromErr(err)
}
username := d.Get("username").(string)

username := resolveUsernameByID(ctx, client, d.Get("user_id").(string), d.Get("username").(string))

_, err = client.Teams.RemoveTeamMembershipByID(ctx, orgId, teamId, username)
if err != nil {
Expand Down
25 changes: 25 additions & 0 deletions github/util.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package github

import (
"context"
"crypto/md5"
"errors"
"fmt"
Expand All @@ -9,6 +10,7 @@ import (
"regexp"
"slices"
"sort"
"strconv"
"strings"

"github.com/google/go-github/v85/github"
Expand Down Expand Up @@ -234,6 +236,29 @@ func validateSecretNameFunc(v any, path cty.Path) diag.Diagnostics {
return wrapErrors(errs)
}

// resolveUsernameByID resolves the current GitHub login for a user by their numeric ID.
// This handles EMU username changes where a suspended user's username becomes an obfuscated hash.
// If userID is empty or the lookup fails, the fallback username is returned unchanged.
func resolveUsernameByID(ctx context.Context, client *github.Client, userID string, fallback string) string {
if userID == "" {
return fallback
}
id, err := strconv.ParseInt(userID, 10, 64)
if err != nil {
log.Printf("[WARN] Could not parse user_id %q: %s", userID, err)
return fallback
}
user, _, err := client.Users.GetByID(ctx, id)
if err != nil {
log.Printf("[WARN] Could not resolve username for user_id %d: %s. Using fallback %q", id, err, fallback)
return fallback
}
if user.GetLogin() != fallback {
log.Printf("[DEBUG] Resolved user_id %d to current username %q (stored username was %q)", id, user.GetLogin(), fallback)
}
return user.GetLogin()
}

// deleteResourceOn404AndSwallow304OtherwiseReturnError will log and delete resource if error is 404 which indicates resource (or any of its ancestors)
// doesn't exist.
// resourceDescription represents a formatting string that represents the resource
Expand Down
3 changes: 3 additions & 0 deletions github/util_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import "strconv"
// userIdentity represents a GitHub user by their login.
type userIdentity struct {
login string
// id is the numeric GitHub user ID, used to resolve the current login
// when the username may have changed (e.g. suspended EMU users).
id int64
}

// userCollaborator represents a GitHub user collaborator with its identity and permission level.
Expand Down