Skip to content

Commit d539aa0

Browse files
committed
feat: add github_actions_runner_group_repository_access resource
Resolves #3375 Currently the only way to control which repositories have access to a specific runner group is via the central runner group resource `github_actions_runner_group`, which manages the list of all repositories that have access to the runner group. In many cases, especially surrounding building Internal Development Platforms which create managed repositories as needed it is not desirable to centralize the list of repositories in a central state and rather allow independent, potentially concurrent deployment processes to add/remove a specific repository. This commit adds a new resource which allows to configure this access on a single repo basis without needing to track the state of all authorized repositories for a runner group
1 parent f494cc6 commit d539aa0

4 files changed

Lines changed: 235 additions & 1 deletion
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
layout: "github"
3+
page_title: "GitHub: github_actions_runner_group_repository_access"
4+
description: |-
5+
Manages a repository's access to an Actions Runner Group within a GitHub organization
6+
---
7+
8+
# github_actions_runner_group_repository_access
9+
10+
This resource allows you to manage repository access to GitHub Actions runner groups within your GitHub (enterprise) organizations independently for each repository.
11+
You must have runner group admin access to an organization to use this resource.
12+
13+
~> **Note:** The action runners group's `visibility` must be `selected` and if also managing the runner group via terraform: `selected_repository_ids` must **not** be set.
14+
15+
## Example Usage
16+
17+
```hcl
18+
resource "github_repository" "example" {
19+
name = "my-repository"
20+
}
21+
22+
resource "github_actions_runner_group" "example" {
23+
name = github_repository.example.name
24+
visibility = "selected"
25+
}
26+
27+
resource "github_actions_runner_group_repository_access" "example" {
28+
runner_group_id = github_actions_runner_group.id
29+
repository_id = github_repository.example.repo_id
30+
}
31+
```
32+
33+
## Argument Reference
34+
35+
The following arguments are supported:
36+
37+
* `runner_group_id` - (Required) Id of the runner group
38+
* `repository_id` - (Required) Id of the repository to give access to the runner group
39+
40+
41+
## Attributes Reference
42+
43+
* `id` - id of this resource, formed as `<runner_group_id>/<repository_id>`
44+
45+
## Import
46+
47+
This resource can be imported using the ID of the runner group and the repository ID:
48+
49+
```
50+
$ terraform import github_actions_runner_group_repository_access.test <runner_group_id>/<repository_id>
51+
```

github/provider.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ func init() {
2020
// NewProvider returns a function that returns the schema.Provider for this provider.
2121
func NewProvider() func() *schema.Provider {
2222
return func() *schema.Provider {
23-
return &schema.Provider{
23+
24+
25+
return &schema.Provider{
2426
Schema: map[string]*schema.Schema{
2527
"base_url": {
2628
Type: schema.TypeString,
@@ -144,6 +146,7 @@ func NewProvider() func() *schema.Provider {
144146
"github_actions_repository_oidc_subject_claim_customization_template": resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate(),
145147
"github_actions_repository_permissions": resourceGithubActionsRepositoryPermissions(),
146148
"github_actions_runner_group": resourceGithubActionsRunnerGroup(),
149+
"github_actions_runner_group_repository_access": resourceGithubActionsRunnerGroupRepositoryAccess(),
147150
"github_actions_hosted_runner": resourceGithubActionsHostedRunner(),
148151
"github_actions_secret": resourceGithubActionsSecret(),
149152
"github_actions_variable": resourceGithubActionsVariable(),
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
)
13+
14+
func resourceGithubActionsRunnerGroupRepositoryAccess() *schema.Resource {
15+
return &schema.Resource{
16+
Description: "Manages the access of a repository to an organization runner group.",
17+
CreateContext: resourceGithubActionsRunnerGroupRepositoryAccessCreate,
18+
ReadContext: resourceGithubActionsRunnerGroupRepositoryAccessRead,
19+
// Omitting update function since this resource expresses a simple membership in the set of repositories with access to the runner group, it either exists or doesn't
20+
// UpdateContext: resourceGithubActionsRunnerGroupRepositoryAccessUpdate,
21+
DeleteContext: resourceGithubActionsRunnerGroupRepositoryAccessDelete,
22+
Importer: &schema.ResourceImporter{
23+
StateContext: resourceGithubActionsRunnerGroupRepositoryAccessImport,
24+
},
25+
26+
Schema: map[string]*schema.Schema{
27+
"runner_group_id": {
28+
Type: schema.TypeInt,
29+
Required: true,
30+
ForceNew: true,
31+
Description: "The ID of the runner group to grant the repository access on",
32+
},
33+
"repository_id": {
34+
Type: schema.TypeInt,
35+
Required: true,
36+
ForceNew: true,
37+
Description: "The ID of the repository to grant access to the runner group",
38+
},
39+
},
40+
}
41+
}
42+
43+
func resourceGithubActionsRunnerGroupRepositoryAccessCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
44+
client := meta.(*Owner).v3client
45+
orgName := meta.(*Owner).name
46+
47+
runnerGroupId := d.Get("runner_group_id").(int)
48+
repositoryId := d.Get("repository_id").(int)
49+
50+
_, err := client.Actions.AddRepositoryAccessRunnerGroup(ctx, orgName, int64(runnerGroupId), int64(repositoryId))
51+
52+
if err != nil {
53+
return diag.FromErr(err)
54+
}
55+
56+
d.SetId(fmt.Sprintf("%d/%d", runnerGroupId, repositoryId))
57+
58+
return resourceGithubActionsRunnerGroupRepositoryAccessRead(ctx, d, meta)
59+
}
60+
61+
func resourceGithubActionsRunnerGroupRepositoryAccessRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
62+
client := meta.(*Owner).v3client
63+
orgName := meta.(*Owner).name
64+
65+
runnerGroupId := d.Get("runner_group_id").(int)
66+
repositoryId := d.Get("repository_id").(int)
67+
68+
for repo, err := range client.Actions.ListRepositoryAccessRunnerGroupIter(ctx, orgName, int64(runnerGroupId), nil) {
69+
if err != nil {
70+
return diag.FromErr(err)
71+
}
72+
73+
if *repo.ID == int64(repositoryId) {
74+
// Resource matches the state in github exactly (repo has access), no need for further modifications
75+
return nil
76+
}
77+
}
78+
// We reached the end of the list without a match for our desired repository access
79+
log.Printf("[INFO] Removing access of repository with id %d to runner group %s/%d from state because access no longer exists in GitHub", repositoryId, orgName, runnerGroupId)
80+
d.SetId("")
81+
return nil
82+
83+
}
84+
85+
func resourceGithubActionsRunnerGroupRepositoryAccessDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
86+
client := meta.(*Owner).v3client
87+
orgName := meta.(*Owner).name
88+
89+
runnerGroupId := d.Get("runner_group_id").(int)
90+
repositoryId := d.Get("repository_id").(int)
91+
92+
_, err := client.Actions.RemoveRepositoryAccessRunnerGroup(ctx, orgName, int64(runnerGroupId), int64(repositoryId))
93+
return diag.FromErr(err)
94+
95+
}
96+
97+
func resourceGithubActionsRunnerGroupRepositoryAccessImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
98+
id := d.Id()
99+
parts := strings.Split(id, "/")
100+
if len(parts) != 2 {
101+
return nil, fmt.Errorf("invalid import specified: supplied import must be written as <runner_group_id>/<repository_id>")
102+
}
103+
104+
runnerGroupID, err := strconv.Atoi(parts[0])
105+
106+
if err != nil {
107+
return nil, fmt.Errorf("runner_group_id in id must be convertible to an int: %w", err)
108+
}
109+
110+
err = d.Set("runner_group_id", runnerGroupID)
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
repositoryId, err := strconv.Atoi(parts[1])
116+
117+
if err != nil {
118+
return nil, fmt.Errorf("runner_id in id must be convertible to an int: %w", err)
119+
}
120+
121+
err = d.Set("repository_id", repositoryId)
122+
if err != nil {
123+
return nil, err
124+
}
125+
126+
return []*schema.ResourceData{d}, nil
127+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
8+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
9+
)
10+
11+
func TestAccGithubActionsRunnerGroupRepositoryAccess(t *testing.T) {
12+
t.Run("set repo access directly and verify import", func(t *testing.T) {
13+
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
14+
repoName := fmt.Sprintf("%srepo-act-runner-%s", testResourcePrefix, randomID)
15+
config := fmt.Sprintf(`
16+
resource "github_repository" "test" {
17+
name = "%s"
18+
vulnerability_alerts = false
19+
auto_init = true
20+
}
21+
22+
resource "github_actions_runner_group" "test" {
23+
name = github_repository.test.name
24+
visibility = "selected"
25+
allows_public_repositories = true
26+
}
27+
28+
resource "github_actions_runner_group_repository_access" "test" {
29+
runner_group_id = github_actions_runner_group.test.id
30+
repository_id = github_repository.test.repo_id
31+
}
32+
`, repoName)
33+
resource.Test(t, resource.TestCase{
34+
PreCheck: func() { skipUnlessHasOrgs(t) },
35+
ProviderFactories: providerFactories,
36+
Steps: []resource.TestStep{
37+
{
38+
Config: config,
39+
Check: resource.ComposeTestCheckFunc(
40+
resource.TestCheckResourceAttrSet("github_actions_runner_group_repository_access.test", "id"),
41+
resource.TestCheckResourceAttrSet("github_actions_runner_group_repository_access.test", "runner_group_id"),
42+
resource.TestCheckResourceAttrSet("github_actions_runner_group_repository_access.test", "repository_id"),
43+
),
44+
},
45+
{
46+
ResourceName: "github_actions_runner_group_repository_access.test",
47+
ImportState: true,
48+
ImportStateVerify: true,
49+
},
50+
},
51+
})
52+
})
53+
}

0 commit comments

Comments
 (0)