Skip to content

Commit bfb6a49

Browse files
authored
[BUG] Enable importing github_emu_group_mapping for Group with multiple teams (#3054)
* Add tests for `github_emu_group_mapping` Signed-off-by: Timo Sand <[email protected]> * Fix broken import of `github_emu_group_mapping`` Which was found by our new tests! Signed-off-by: Timo Sand <[email protected]> * Add failing test to verify problem in #3030 Signed-off-by: Timo Sand <[email protected]> * Update import function to support a single ID and a two part ID Signed-off-by: Timo Sand <[email protected]> * Refactor to use Context-aware functions Signed-off-by: Timo Sand <[email protected]> * Add detailed logging for INFO, DEBUG and TRACE levels Signed-off-by: Timo Sand <[email protected]> * Extract import functions into `util_import.go` Signed-off-by: Timo Sand <[email protected]> * Address review comments Signed-off-by: Timo Sand <[email protected]> * Merge Create and Update and remove call to Read Signed-off-by: Timo Sand <[email protected]> * Address review comments Signed-off-by: Timo Sand <[email protected]> * Use SDK accessors instead of pointers Signed-off-by: Timo Sand <[email protected]> * Add `getTeamSlugContext` func Signed-off-by: Timo Sand <[email protected]> * Replace `TeamName` with fetching TeamSlug from API Signed-off-by: Timo Sand <[email protected]> * Only support import with two part ID Signed-off-by: Timo Sand <[email protected]> * Add warning about Read misbehaving Signed-off-by: Timo Sand <[email protected]> --------- Signed-off-by: Timo Sand <[email protected]>
1 parent b6fc505 commit bfb6a49

6 files changed

Lines changed: 368 additions & 72 deletions

File tree

CONTRIBUTING.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ If a full debugger is desired, VSCode may be used. In order to do so,
7878

7979
```json
8080
{
81-
"name": "Attach to Process",
82-
"type": "go",
83-
"request": "attach",
84-
"mode": "local",
85-
"processId": 0,
81+
"name": "Attach to Process",
82+
"type": "go",
83+
"request": "attach",
84+
"mode": "local",
85+
"processId": 0,
8686
}
8787
```
8888

@@ -165,7 +165,7 @@ export GITHUB_TOKEN=
165165
# Configure user level values
166166
export GH_TEST_USER_REPOSITORY=
167167

168-
# Configure for the org under test
168+
# Configure values for the organization under test
169169
export GH_TEST_ORG_USER=
170170
export GH_TEST_ORG_SECRET_NAME=
171171
export GH_TEST_ORG_REPOSITORY=
@@ -177,6 +177,9 @@ export GH_TEST_EXTERNAL_USER=
177177
export GH_TEST_EXTERNAL_USER_TOKEN=
178178
export GH_TEST_EXTERNAL_USER2=
179179

180+
# Configure values for the enterprise under test
181+
export GH_TEST_ENTERPRISE_EMU_GROUP_ID=
182+
180183
# Configure test options
181184
export GH_TEST_ADVANCED_SECURITY=
182185
```

github/acc_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ type testAccConfig struct {
6868
testExternalUserToken string
6969
testExternalUser2 string
7070

71+
// Enterprise test configuration
72+
testEnterpriseEMUGroupId int
73+
7174
// Test options
7275
testAdvancedSecurity bool
7376
}
@@ -149,6 +152,11 @@ func TestMain(m *testing.M) {
149152
fmt.Println("GITHUB_ENTERPRISE_SLUG environment variable not set")
150153
os.Exit(1)
151154
}
155+
156+
i, err := strconv.Atoi(os.Getenv("GH_TEST_ENTERPRISE_EMU_GROUP_ID"))
157+
if err == nil {
158+
config.testEnterpriseEMUGroupId = i
159+
}
152160
}
153161

154162
i, err := strconv.Atoi(os.Getenv("GH_TEST_ORG_APP_INSTALLATION_ID"))

github/resource_github_emu_group_mapping.go

Lines changed: 152 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,19 @@ import (
66
"strconv"
77

88
"github.com/google/go-github/v81/github"
9+
"github.com/hashicorp/terraform-plugin-log/tflog"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
911
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1012
)
1113

1214
func resourceGithubEMUGroupMapping() *schema.Resource {
1315
return &schema.Resource{
14-
Create: resourceGithubEMUGroupMappingCreate,
15-
Read: resourceGithubEMUGroupMappingRead,
16-
Update: resourceGithubEMUGroupMappingUpdate,
17-
Delete: resourceGithubEMUGroupMappingDelete,
16+
CreateContext: resourceGithubEMUGroupMappingCreateOrUpdate,
17+
ReadContext: resourceGithubEMUGroupMappingRead,
18+
UpdateContext: resourceGithubEMUGroupMappingCreateOrUpdate,
19+
DeleteContext: resourceGithubEMUGroupMappingDelete,
1820
Importer: &schema.ResourceImporter{
19-
State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
20-
id, err := strconv.Atoi(d.Id())
21-
if err != nil {
22-
return nil, err
23-
}
24-
if err := d.Set("group_id", id); err != nil {
25-
return nil, err
26-
}
27-
ctx := context.WithValue(context.Background(), ctxId, d.Id())
28-
client := meta.(*Owner).v3client
29-
orgName := meta.(*Owner).name
30-
group, _, err := client.Teams.GetExternalGroup(ctx, orgName, int64(id))
31-
if err != nil {
32-
return nil, err
33-
}
34-
if len(group.Teams) != 1 {
35-
return nil, fmt.Errorf("could not get team_slug from %v number of teams", len(group.Teams))
36-
}
37-
if err := d.Set("team_slug", group.Teams[0].TeamName); err != nil {
38-
return nil, err
39-
}
40-
d.SetId(fmt.Sprintf("teams/%s/external-groups", d.Id()))
41-
return []*schema.ResourceData{d}, nil
42-
},
21+
StateContext: resourceGithubEMUGroupMappingImport,
4322
},
4423
Schema: map[string]*schema.Schema{
4524
"team_slug": {
@@ -60,110 +39,188 @@ func resourceGithubEMUGroupMapping() *schema.Resource {
6039
}
6140
}
6241

63-
func resourceGithubEMUGroupMappingCreate(d *schema.ResourceData, meta any) error {
64-
return resourceGithubEMUGroupMappingUpdate(d, meta)
65-
}
42+
func resourceGithubEMUGroupMappingRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
43+
tflog.Trace(ctx, "Reading EMU group mapping", map[string]any{
44+
"resource_id": d.Id(),
45+
})
6646

67-
func resourceGithubEMUGroupMappingRead(d *schema.ResourceData, meta any) error {
6847
err := checkOrganization(meta)
6948
if err != nil {
70-
return err
49+
return diag.FromErr(err)
7150
}
7251
client := meta.(*Owner).v3client
7352
orgName := meta.(*Owner).name
7453

7554
id, ok := d.GetOk("group_id")
7655
if !ok {
77-
return fmt.Errorf("could not get group id from provided value")
56+
return diag.Errorf("could not get group id from provided value")
7857
}
7958
id64, err := getInt64FromInterface(id)
8059
if err != nil {
81-
return err
60+
return diag.FromErr(err)
8261
}
8362

84-
ctx := context.WithValue(context.Background(), ctxId, d.Id())
63+
tflog.Debug(ctx, "Querying external group from GitHub API", map[string]any{
64+
"org_name": orgName,
65+
"group_id": id64,
66+
})
8567

8668
group, resp, err := client.Teams.GetExternalGroup(ctx, orgName, id64)
8769
if err != nil {
8870
if resp != nil && resp.StatusCode == 404 {
8971
// If the group is not found, remove it from state
72+
tflog.Info(ctx, "Removing EMU group mapping from state because it no longer exists in GitHub", map[string]any{
73+
"org_name": orgName,
74+
"group_id": id64,
75+
"resource_id": d.Id(),
76+
"status_code": resp.StatusCode,
77+
})
9078
d.SetId("")
9179
return nil
9280
}
93-
return err
81+
return diag.FromErr(err)
9482
}
9583

84+
tflog.Debug(ctx, "Successfully retrieved external group from GitHub API", map[string]any{
85+
"org_name": orgName,
86+
"group_id": id64,
87+
"team_count": len(group.Teams),
88+
})
89+
9690
if len(group.Teams) < 1 {
9791
// if there's not a team linked, that means it was removed outside of terraform
9892
// and we should remove it from our state
93+
tflog.Info(ctx, "Removing EMU group mapping from state because no teams are linked", map[string]any{
94+
"org_name": orgName,
95+
"group_id": id64,
96+
"resource_id": d.Id(),
97+
})
9998
d.SetId("")
10099
return nil
101100
}
102101

103-
if err = d.Set("etag", resp.Header.Get("ETag")); err != nil {
104-
return err
102+
etag := resp.Header.Get("ETag")
103+
tflog.Trace(ctx, "Setting state attribute: etag", map[string]any{
104+
"etag": etag,
105+
})
106+
if err = d.Set("etag", etag); err != nil {
107+
return diag.FromErr(err)
105108
}
106-
if err = d.Set("group_id", int(*group.GroupID)); err != nil {
107-
return err
109+
110+
groupIDInt := int(group.GetGroupID())
111+
tflog.Trace(ctx, "Setting state attribute: group_id", map[string]any{
112+
"group_id": groupIDInt,
113+
})
114+
if err = d.Set("group_id", groupIDInt); err != nil {
115+
return diag.FromErr(err)
108116
}
109117
return nil
110118
}
111119

112-
func resourceGithubEMUGroupMappingUpdate(d *schema.ResourceData, meta any) error {
120+
func resourceGithubEMUGroupMappingCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
121+
resourceID := d.Id()
122+
tflog.Trace(ctx, "Creating or updating EMU group mapping", map[string]any{
123+
"resource_id": resourceID,
124+
})
125+
113126
err := checkOrganization(meta)
114127
if err != nil {
115-
return err
128+
return diag.FromErr(err)
116129
}
117130
client := meta.(*Owner).v3client
118131
orgName := meta.(*Owner).name
119-
ctx := context.WithValue(context.Background(), ctxId, d.Id())
120132

121133
teamSlug, ok := d.GetOk("team_slug")
122134
if !ok {
123-
return fmt.Errorf("could not get team slug from provided value")
135+
return diag.Errorf("could not get team slug from provided value")
124136
}
125137

126138
id, ok := d.GetOk("group_id")
127139
if !ok {
128-
return fmt.Errorf("could not get group id from provided value")
140+
return diag.Errorf("could not get group id from provided value")
129141
}
130142
id64, err := getInt64FromInterface(id)
131143
if err != nil {
132-
return err
144+
return diag.FromErr(err)
133145
}
134146

147+
teamSlugStr := teamSlug.(string)
148+
135149
eg := &github.ExternalGroup{
136150
GroupID: &id64,
137151
}
138152

139-
_, _, err = client.Teams.UpdateConnectedExternalGroup(ctx, orgName, teamSlug.(string), eg)
153+
tflog.Debug(ctx, "Updating connected external group via GitHub API", map[string]any{
154+
"org_name": orgName,
155+
"team_slug": teamSlugStr,
156+
"group_id": id64,
157+
})
158+
159+
_, resp, err := client.Teams.UpdateConnectedExternalGroup(ctx, orgName, teamSlugStr, eg)
140160
if err != nil {
141-
return err
161+
return diag.FromErr(err)
142162
}
143163

144-
d.SetId(fmt.Sprintf("teams/%s/external-groups", teamSlug))
145-
return resourceGithubEMUGroupMappingRead(d, meta)
164+
tflog.Debug(ctx, "Successfully updated connected external group", map[string]any{
165+
"org_name": orgName,
166+
"team_slug": teamSlugStr,
167+
"group_id": id64,
168+
})
169+
170+
newResourceID := fmt.Sprintf("teams/%s/external-groups", teamSlugStr)
171+
tflog.Trace(ctx, "Setting resource ID", map[string]any{
172+
"resource_id": newResourceID,
173+
})
174+
d.SetId(newResourceID)
175+
176+
etag := resp.Header.Get("ETag")
177+
tflog.Trace(ctx, "Setting state attribute: etag", map[string]any{
178+
"etag": etag,
179+
})
180+
if err = d.Set("etag", etag); err != nil {
181+
return diag.FromErr(err)
182+
}
183+
184+
tflog.Trace(ctx, "Resource created or updated successfully", map[string]any{
185+
"resource_id": newResourceID,
186+
})
187+
return nil
146188
}
147189

148-
func resourceGithubEMUGroupMappingDelete(d *schema.ResourceData, meta any) error {
190+
func resourceGithubEMUGroupMappingDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
191+
tflog.Trace(ctx, "Deleting EMU group mapping", map[string]any{
192+
"resource_id": d.Id(),
193+
})
194+
149195
err := checkOrganization(meta)
150196
if err != nil {
151-
return err
197+
return diag.FromErr(err)
152198
}
153199
client := meta.(*Owner).v3client
154200
orgName := meta.(*Owner).name
155201

156202
teamSlug, ok := d.GetOk("team_slug")
157203
if !ok {
158-
return fmt.Errorf("could not parse team slug from provided value")
204+
return diag.Errorf("could not parse team slug from provided value")
159205
}
160206

161-
ctx := context.WithValue(context.Background(), ctxId, d.Id())
207+
teamSlugStr := teamSlug.(string)
208+
tflog.Debug(ctx, "Removing connected external group from team via GitHub API", map[string]any{
209+
"org_name": orgName,
210+
"team_slug": teamSlugStr,
211+
"resource_id": d.Id(),
212+
})
162213

163-
_, err = client.Teams.RemoveConnectedExternalGroup(ctx, orgName, teamSlug.(string))
214+
_, err = client.Teams.RemoveConnectedExternalGroup(ctx, orgName, teamSlugStr)
164215
if err != nil {
165-
return err
216+
return diag.FromErr(err)
166217
}
218+
219+
tflog.Debug(ctx, "Successfully removed connected external group from team", map[string]any{
220+
"org_name": orgName,
221+
"team_slug": teamSlugStr,
222+
"resource_id": d.Id(),
223+
})
167224
return nil
168225
}
169226

@@ -185,3 +242,41 @@ func getInt64FromInterface(val any) (int64, error) {
185242
}
186243
return id64, nil
187244
}
245+
246+
func resourceGithubEMUGroupMappingImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
247+
importID := d.Id()
248+
tflog.Trace(ctx, "Importing EMU group mapping with two-part ID", map[string]any{
249+
"import_id": importID,
250+
"strategy": "two_part_id",
251+
})
252+
253+
groupIDString, teamSlug, err := parseTwoPartID(d.Id(), "group_id", "team_slug")
254+
if err != nil {
255+
return nil, err
256+
}
257+
groupID, err := strconv.Atoi(groupIDString)
258+
if err != nil {
259+
return nil, err
260+
}
261+
262+
tflog.Debug(ctx, "Parsed two-part import ID", map[string]any{
263+
"import_id": importID,
264+
"group_id": groupID,
265+
"team_slug": teamSlug,
266+
})
267+
268+
if err := d.Set("group_id", groupID); err != nil {
269+
return nil, err
270+
}
271+
272+
if err := d.Set("team_slug", teamSlug); err != nil {
273+
return nil, err
274+
}
275+
276+
resourceID := fmt.Sprintf("teams/%s/external-groups", teamSlug)
277+
tflog.Trace(ctx, "Setting resource ID", map[string]any{
278+
"resource_id": resourceID,
279+
})
280+
d.SetId(resourceID)
281+
return []*schema.ResourceData{d}, nil
282+
}

0 commit comments

Comments
 (0)