Skip to content

Commit 0354aac

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.
1 parent add2157 commit 0354aac

4 files changed

Lines changed: 861 additions & 0 deletions

github/provider.go

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

0 commit comments

Comments
 (0)