Skip to content

Commit 968b29b

Browse files
committed
feat: add organization inherited runner group settings resource
Add new resource `github_organization_inherited_runner_group_settings`. This resource allows managing organization-level settings for enterprise Actions runner groups that are inherited by an organization. It configures various org-specific settings such as allowed repositories, visibility and workflow restrictions. The existing org-level resource assumes that an organization runner group is to be created & fully managed by Terraform so it does not work for an enterprise-scope group that is shared with one or more organizations within a GitHub Enterprise environment. Resolves #2768
1 parent 1af72d4 commit 968b29b

4 files changed

Lines changed: 874 additions & 0 deletions

github/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ func Provider() *schema.Provider {
216216
"github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(),
217217
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
218218
"github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(),
219+
"github_organization_inherited_runner_group_settings": resourceGithubOrganizationInheritedRunnerGroupSettings(),
219220
"github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
220221
"github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(),
221222
},
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"strconv"
9+
10+
"github.com/google/go-github/v83/github"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
14+
)
15+
16+
func resourceGithubOrganizationInheritedRunnerGroupSettings() *schema.Resource {
17+
return &schema.Resource{
18+
Description: "Manages organization-level settings for an enterprise Actions runner group inherited by the organization.",
19+
20+
CreateContext: resourceGithubOrganizationInheritedRunnerGroupSettingsCreate,
21+
ReadContext: resourceGithubOrganizationInheritedRunnerGroupSettingsRead,
22+
UpdateContext: resourceGithubOrganizationInheritedRunnerGroupSettingsUpdate,
23+
DeleteContext: resourceGithubOrganizationInheritedRunnerGroupSettingsDelete,
24+
Importer: &schema.ResourceImporter{
25+
StateContext: resourceGithubOrganizationInheritedRunnerGroupSettingsImport,
26+
},
27+
28+
Schema: map[string]*schema.Schema{
29+
"organization": {
30+
Type: schema.TypeString,
31+
Required: true,
32+
ForceNew: true,
33+
Description: "The GitHub organization name.",
34+
},
35+
"enterprise_runner_group_name": {
36+
Type: schema.TypeString,
37+
Required: true,
38+
Description: "The name of the enterprise runner group inherited by the organization.",
39+
},
40+
"runner_group_id": {
41+
Type: schema.TypeInt,
42+
Computed: true,
43+
Description: "The ID of the inherited enterprise runner group in the organization.",
44+
},
45+
"inherited": {
46+
Type: schema.TypeBool,
47+
Computed: true,
48+
Description: "Whether this runner group is inherited from the enterprise.",
49+
},
50+
"visibility": {
51+
Type: schema.TypeString,
52+
Optional: true,
53+
Default: "selected",
54+
Description: "The visibility of the runner group. Can be 'all', 'selected', or 'private'.",
55+
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "selected", "private"}, false)),
56+
},
57+
"selected_repository_ids": {
58+
Type: schema.TypeSet,
59+
Elem: &schema.Schema{
60+
Type: schema.TypeInt,
61+
},
62+
Set: schema.HashInt,
63+
Optional: true,
64+
Description: "List of repository IDs that can access the runner group. Only applicable when visibility is set to 'selected'.",
65+
},
66+
"allows_public_repositories": {
67+
Type: schema.TypeBool,
68+
Optional: true,
69+
Default: false,
70+
Description: "Whether public repositories can be added to the runner group.",
71+
},
72+
"restricted_to_workflows": {
73+
Type: schema.TypeBool,
74+
Optional: true,
75+
Default: false,
76+
Description: "If 'true', the runner group will be restricted to running only the workflows specified in the 'selected_workflows' array. Defaults to 'false'.",
77+
},
78+
"selected_workflows": {
79+
Type: schema.TypeList,
80+
Elem: &schema.Schema{Type: schema.TypeString},
81+
Optional: true,
82+
Description: "List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to 'true'.",
83+
},
84+
},
85+
}
86+
}
87+
88+
func findInheritedEnterpriseRunnerGroupByName(client *github.Client, ctx context.Context, org string, name string) (*github.RunnerGroup, error) {
89+
opts := &github.ListOrgRunnerGroupOptions{
90+
ListOptions: github.ListOptions{
91+
PerPage: maxPerPage,
92+
},
93+
}
94+
95+
for {
96+
groups, resp, err := client.Actions.ListOrganizationRunnerGroups(ctx, org, opts)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
for _, group := range groups.RunnerGroups {
102+
if group.GetInherited() && group.GetName() == name {
103+
return group, nil
104+
}
105+
}
106+
107+
if resp.NextPage == 0 {
108+
break
109+
}
110+
opts.Page = resp.NextPage
111+
}
112+
113+
return nil, fmt.Errorf("inherited enterprise runner group '%s' not found in organization '%s'. Ensure the enterprise runner group is shared with this organization", name, org)
114+
}
115+
116+
func resourceGithubOrganizationInheritedRunnerGroupSettingsCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
117+
client := meta.(*Owner).v3client
118+
119+
org := d.Get("organization").(string)
120+
enterpriseRunnerGroupName := d.Get("enterprise_runner_group_name").(string)
121+
visibility := d.Get("visibility").(string)
122+
allowsPublicRepositories := d.Get("allows_public_repositories").(bool)
123+
restrictedToWorkflows := d.Get("restricted_to_workflows").(bool)
124+
selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids")
125+
126+
selectedWorkflows := []string{}
127+
if workflows, ok := d.GetOk("selected_workflows"); ok {
128+
for _, workflow := range workflows.([]any) {
129+
selectedWorkflows = append(selectedWorkflows, workflow.(string))
130+
}
131+
}
132+
133+
// Find the inherited enterprise runner group by name
134+
runnerGroup, err := findInheritedEnterpriseRunnerGroupByName(client, ctx, org, enterpriseRunnerGroupName)
135+
if err != nil {
136+
return diag.FromErr(err)
137+
}
138+
139+
runnerGroupID := runnerGroup.GetID()
140+
id, err := buildID(org, strconv.FormatInt(runnerGroupID, 10))
141+
if err != nil {
142+
return diag.FromErr(err)
143+
}
144+
d.SetId(id)
145+
146+
if err := d.Set("runner_group_id", int(runnerGroupID)); err != nil {
147+
return diag.FromErr(err)
148+
}
149+
if err := d.Set("inherited", runnerGroup.GetInherited()); err != nil {
150+
return diag.FromErr(err)
151+
}
152+
153+
// Update runner group settings
154+
updateReq := github.UpdateRunnerGroupRequest{
155+
Visibility: github.Ptr(visibility),
156+
AllowsPublicRepositories: github.Ptr(allowsPublicRepositories),
157+
RestrictedToWorkflows: github.Ptr(restrictedToWorkflows),
158+
SelectedWorkflows: selectedWorkflows,
159+
}
160+
161+
_, _, err = client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq)
162+
if err != nil {
163+
return diag.Errorf("failed to update runner group: %s", err)
164+
}
165+
166+
// Set repository access if visibility is "selected"
167+
if visibility == "selected" && hasSelectedRepositories {
168+
selectedRepositoryIDs := []int64{}
169+
for _, id := range selectedRepositories.(*schema.Set).List() {
170+
selectedRepositoryIDs = append(selectedRepositoryIDs, int64(id.(int)))
171+
}
172+
173+
repoAccessReq := github.SetRepoAccessRunnerGroupRequest{
174+
SelectedRepositoryIDs: selectedRepositoryIDs,
175+
}
176+
177+
_, err = client.Actions.SetRepositoryAccessRunnerGroup(ctx, org, runnerGroupID, repoAccessReq)
178+
if err != nil {
179+
return diag.Errorf("failed to set repository access: %s", err)
180+
}
181+
}
182+
183+
return nil
184+
}
185+
186+
func resourceGithubOrganizationInheritedRunnerGroupSettingsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
187+
client := meta.(*Owner).v3client
188+
189+
org := d.Get("organization").(string)
190+
runnerGroupID := int64(d.Get("runner_group_id").(int))
191+
192+
// Get the runner group details
193+
runnerGroup, _, err := client.Actions.GetOrganizationRunnerGroup(ctx, org, runnerGroupID)
194+
if err != nil {
195+
if ghErr, ok := err.(*github.ErrorResponse); ok {
196+
if ghErr.Response.StatusCode == http.StatusNotFound {
197+
log.Printf("[INFO] Removing actions organization runner group %s from state because it no longer exists in GitHub",
198+
d.Id())
199+
d.SetId("")
200+
return nil
201+
}
202+
}
203+
return diag.FromErr(err)
204+
}
205+
206+
if err := d.Set("inherited", runnerGroup.GetInherited()); err != nil {
207+
return diag.FromErr(err)
208+
}
209+
210+
if err := d.Set("visibility", runnerGroup.GetVisibility()); err != nil {
211+
return diag.FromErr(err)
212+
}
213+
214+
// Get repository access list only if visibility is "selected"
215+
if runnerGroup.GetVisibility() == "selected" {
216+
selectedRepositoryIDs := []int64{}
217+
opts := &github.ListOptions{
218+
PerPage: maxPerPage,
219+
}
220+
221+
for {
222+
repos, resp, err := client.Actions.ListRepositoryAccessRunnerGroup(ctx, org, runnerGroupID, opts)
223+
if err != nil {
224+
return diag.Errorf("failed to list repository access: %s", err)
225+
}
226+
227+
for _, repo := range repos.Repositories {
228+
selectedRepositoryIDs = append(selectedRepositoryIDs, repo.GetID())
229+
}
230+
231+
if resp.NextPage == 0 {
232+
break
233+
}
234+
opts.Page = resp.NextPage
235+
}
236+
237+
if err := d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil {
238+
return diag.FromErr(err)
239+
}
240+
} else {
241+
if err := d.Set("selected_repository_ids", []int64{}); err != nil {
242+
return diag.FromErr(err)
243+
}
244+
}
245+
246+
if err := d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()); err != nil {
247+
return diag.FromErr(err)
248+
}
249+
250+
if err := d.Set("restricted_to_workflows", runnerGroup.GetRestrictedToWorkflows()); err != nil {
251+
return diag.FromErr(err)
252+
}
253+
254+
if err := d.Set("selected_workflows", runnerGroup.SelectedWorkflows); err != nil {
255+
return diag.FromErr(err)
256+
}
257+
258+
return nil
259+
}
260+
261+
func resourceGithubOrganizationInheritedRunnerGroupSettingsUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
262+
client := meta.(*Owner).v3client
263+
264+
org := d.Get("organization").(string)
265+
runnerGroupID := int64(d.Get("runner_group_id").(int))
266+
visibility := d.Get("visibility").(string)
267+
selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids")
268+
269+
// Update runner group settings if any relevant fields changed
270+
if d.HasChange("visibility") || d.HasChange("allows_public_repositories") || d.HasChange("restricted_to_workflows") || d.HasChange("selected_workflows") {
271+
allowsPublicRepositories := d.Get("allows_public_repositories").(bool)
272+
restrictedToWorkflows := d.Get("restricted_to_workflows").(bool)
273+
274+
selectedWorkflows := []string{}
275+
if workflows, ok := d.GetOk("selected_workflows"); ok {
276+
for _, workflow := range workflows.([]any) {
277+
selectedWorkflows = append(selectedWorkflows, workflow.(string))
278+
}
279+
}
280+
281+
updateReq := github.UpdateRunnerGroupRequest{
282+
Visibility: github.Ptr(visibility),
283+
AllowsPublicRepositories: github.Ptr(allowsPublicRepositories),
284+
RestrictedToWorkflows: github.Ptr(restrictedToWorkflows),
285+
SelectedWorkflows: selectedWorkflows,
286+
}
287+
288+
_, _, err := client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq)
289+
if err != nil {
290+
return diag.Errorf("failed to update runner group: %s", err)
291+
}
292+
}
293+
294+
// Update repository access if changed and visibility is "selected"
295+
if d.HasChange("selected_repository_ids") && visibility == "selected" && hasSelectedRepositories {
296+
selectedRepositoryIDs := []int64{}
297+
298+
for _, id := range selectedRepositories.(*schema.Set).List() {
299+
selectedRepositoryIDs = append(selectedRepositoryIDs, int64(id.(int)))
300+
}
301+
302+
repoAccessReq := github.SetRepoAccessRunnerGroupRequest{
303+
SelectedRepositoryIDs: selectedRepositoryIDs,
304+
}
305+
306+
_, err := client.Actions.SetRepositoryAccessRunnerGroup(ctx, org, runnerGroupID, repoAccessReq)
307+
if err != nil {
308+
return diag.Errorf("failed to set repository access: %s", err)
309+
}
310+
}
311+
312+
return nil
313+
}
314+
315+
func resourceGithubOrganizationInheritedRunnerGroupSettingsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
316+
client := meta.(*Owner).v3client
317+
318+
org := d.Get("organization").(string)
319+
runnerGroupID := int64(d.Get("runner_group_id").(int))
320+
321+
log.Printf("[INFO] Removing repository access for runner group: %s", d.Id())
322+
323+
// Reset to "all" visibility and clear repository access
324+
updateReq := github.UpdateRunnerGroupRequest{
325+
Visibility: github.Ptr("all"),
326+
}
327+
328+
_, _, err := client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq)
329+
if err != nil {
330+
// If the runner group doesn't exist, that's fine
331+
if ghErr, ok := err.(*github.ErrorResponse); ok {
332+
if ghErr.Response.StatusCode == http.StatusNotFound {
333+
return nil
334+
}
335+
}
336+
return diag.Errorf("failed to reset runner group visibility: %s", err)
337+
}
338+
339+
return nil
340+
}
341+
342+
func resourceGithubOrganizationInheritedRunnerGroupSettingsImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
343+
org, identifier, err := parseID2(d.Id())
344+
if err != nil {
345+
return nil, fmt.Errorf("invalid import ID format, expected 'organization:enterprise_runner_group_name' or 'organization:organization_runner_group_id'")
346+
}
347+
348+
client := meta.(*Owner).v3client
349+
350+
var runnerGroup *github.RunnerGroup
351+
352+
// Try to parse as ID first
353+
if id, parseErr := strconv.ParseInt(identifier, 10, 64); parseErr == nil {
354+
// It's an ID - get the runner group and verify it's inherited
355+
runnerGroup, _, err = client.Actions.GetOrganizationRunnerGroup(ctx, org, id)
356+
if err != nil {
357+
return nil, fmt.Errorf("failed to get runner group: %w", err)
358+
}
359+
} else {
360+
// It's a name - find the inherited enterprise runner group
361+
runnerGroup, err = findInheritedEnterpriseRunnerGroupByName(client, ctx, org, identifier)
362+
if err != nil {
363+
return nil, err
364+
}
365+
}
366+
367+
// Verify the runner group is inherited from the enterprise
368+
if !runnerGroup.GetInherited() {
369+
return nil, fmt.Errorf("runner group '%s' is not inherited from the enterprise. This resource only manages inherited enterprise runner groups", runnerGroup.GetName())
370+
}
371+
372+
id, err := buildID(org, strconv.FormatInt(runnerGroup.GetID(), 10))
373+
if err != nil {
374+
return nil, err
375+
}
376+
d.SetId(id)
377+
d.Set("organization", org)
378+
d.Set("enterprise_runner_group_name", runnerGroup.GetName())
379+
d.Set("runner_group_id", int(runnerGroup.GetID()))
380+
d.Set("inherited", runnerGroup.GetInherited())
381+
382+
return []*schema.ResourceData{d}, nil
383+
}

0 commit comments

Comments
 (0)