Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 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
8 changes: 4 additions & 4 deletions examples/hosted_runner/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ resource "github_actions_hosted_runner" "advanced" {
source = "github"
}

size = "8-core"
runner_group_id = github_actions_runner_group.example.id
maximum_runners = 10
enable_static_ip = true
size = "8-core"
runner_group_id = github_actions_runner_group.example.id
maximum_runners = 10
public_ip_enabled = true
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func Provider() *schema.Provider {
"github_organization_block": resourceOrganizationBlock(),
"github_organization_custom_role": resourceGithubOrganizationCustomRole(),
"github_organization_custom_properties": resourceGithubOrganizationCustomProperties(),
"github_organization_network_configuration": resourceGithubOrganizationNetworkConfiguration(),
"github_organization_project": resourceGithubOrganizationProject(),
"github_organization_repository_role": resourceGithubOrganizationRepositoryRole(),
"github_organization_role": resourceGithubOrganizationRole(),
Expand Down
105 changes: 94 additions & 11 deletions github/resource_github_actions_runner_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

type organizationRunnerGroup struct {
NetworkConfigurationID *string `json:"network_configuration_id,omitempty"`
}

func resourceGithubActionsRunnerGroup() *schema.Resource {
return &schema.Resource{
Create: resourceGithubActionsRunnerGroupCreate,
Expand Down Expand Up @@ -92,10 +96,74 @@ func resourceGithubActionsRunnerGroup() *schema.Resource {
Optional: true,
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'.",
},
"network_configuration_id": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringLenBetween(1, 255),
Description: "The identifier of the hosted compute network configuration to associate with this runner group for GitHub-hosted private networking.",
},
},
}
}

func getOrganizationRunnerGroupNetworking(client *github.Client, ctx context.Context, org string, groupID int64) (*organizationRunnerGroup, *github.Response, error) {
req, err := client.NewRequest("GET", fmt.Sprintf("orgs/%s/actions/runner-groups/%d", org, groupID), nil)
if err != nil {
return nil, nil, err
}

var runnerGroup organizationRunnerGroup
resp, err := client.Do(ctx, req, &runnerGroup)
if err != nil {
return nil, resp, err
}

return &runnerGroup, resp, nil
}

func getOrganizationRunnerGroup(client *github.Client, ctx context.Context, org string, groupID int64) (*github.RunnerGroup, *github.Response, error) {
runnerGroup, resp, err := client.Actions.GetOrganizationRunnerGroup(ctx, org, groupID)
if err != nil {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) {
// ignore error StatusNotModified
Comment thread
austenstone marked this conversation as resolved.
return runnerGroup, resp, nil
}
}
return runnerGroup, resp, err
}

func updateOrganizationRunnerGroupNetworking(client *github.Client, ctx context.Context, org string, groupID int64, networkConfigurationID *string) (*github.Response, error) {
payload := map[string]any{
"network_configuration_id": networkConfigurationID,
}

req, err := client.NewRequest("PATCH", fmt.Sprintf("orgs/%s/actions/runner-groups/%d", org, groupID), payload)
if err != nil {
return nil, err
}

resp, err := client.Do(ctx, req, nil)
if err != nil {
return resp, err
}

return resp, nil
}

func setGithubActionsRunnerGroupNetworkingState(d *schema.ResourceData, runnerGroup *organizationRunnerGroup) error {
if runnerGroup != nil && runnerGroup.NetworkConfigurationID != nil && *runnerGroup.NetworkConfigurationID != "" {
if err := d.Set("network_configuration_id", *runnerGroup.NetworkConfigurationID); err != nil {
return err
}
} else {
if err := d.Set("network_configuration_id", nil); err != nil {
return err
}
}
return nil
}

func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta any) error {
err := checkOrganization(meta)
if err != nil {
Expand Down Expand Up @@ -186,19 +254,14 @@ func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta any) er
return err
}

return resourceGithubActionsRunnerGroupRead(d, meta)
}

func getOrganizationRunnerGroup(client *github.Client, ctx context.Context, org string, groupID int64) (*github.RunnerGroup, *github.Response, error) {
runnerGroup, resp, err := client.Actions.GetOrganizationRunnerGroup(ctx, org, groupID)
if err != nil {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) {
// ignore error StatusNotModified
return runnerGroup, resp, nil
if networkConfigurationID, ok := d.GetOk("network_configuration_id"); ok {
networkConfigurationIDValue := networkConfigurationID.(string)
if _, err = updateOrganizationRunnerGroupNetworking(client, ctx, orgName, runnerGroup.GetID(), &networkConfigurationIDValue); err != nil {
Comment thread
austenstone marked this conversation as resolved.
return err
}
}
return runnerGroup, resp, err

return resourceGithubActionsRunnerGroupRead(d, meta)
Comment thread
austenstone marked this conversation as resolved.
Outdated
}

func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) error {
Expand Down Expand Up @@ -272,6 +335,14 @@ func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) erro
return err
}

runnerGroupNetworking, _, err := getOrganizationRunnerGroupNetworking(client, context.WithValue(context.Background(), ctxId, d.Id()), orgName, runnerGroupID)
if err != nil {
return err
}
if err = setGithubActionsRunnerGroupNetworkingState(d, runnerGroupNetworking); err != nil {
return err
}

selectedRepositoryIDs := []int64{}
options := github.ListOptions{
PerPage: maxPerPage,
Expand Down Expand Up @@ -339,6 +410,18 @@ func resourceGithubActionsRunnerGroupUpdate(d *schema.ResourceData, meta any) er
return err
}

if d.HasChange("network_configuration_id") {
var networkConfigurationIDValue *string
if networkConfigurationID, ok := d.GetOk("network_configuration_id"); ok {
value := networkConfigurationID.(string)
networkConfigurationIDValue = &value
}

if _, err := updateOrganizationRunnerGroupNetworking(client, ctx, orgName, runnerGroupID, networkConfigurationIDValue); err != nil {
return err
}
}

selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids")
selectedRepositoryIDs := []int64{}

Expand Down
132 changes: 132 additions & 0 deletions github/resource_github_actions_runner_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName string) resource.TestCheckFunc {
return func(state *terraform.State) error {
runnerGroup, ok := state.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("runner group resource %s not found in state", resourceName)
}

networkConfiguration, ok := state.RootModule().Resources[networkConfigurationResourceName]
if !ok {
return fmt.Errorf("network configuration resource %s not found in state", networkConfigurationResourceName)
}

actual := runnerGroup.Primary.Attributes["network_configuration_id"]
expected := networkConfiguration.Primary.ID

if actual != expected {
return fmt.Errorf("actual network_configuration_id %q does not match expected %q", actual, expected)
}

return nil
}
}

func TestAccGithubActionsRunnerGroup(t *testing.T) {
t.Run("creates runner groups without error", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
Expand Down Expand Up @@ -97,6 +120,115 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) {
})
})

t.Run("manages private networking association for hosted runners", func(t *testing.T) {
networkSettingsID := testAccOrganizationNetworkConfigurationID(t)
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
resourceName := "github_actions_runner_group.test"
networkConfigurationResourceName := "github_organization_network_configuration.test"
networkConfigurationName := fmt.Sprintf("%snetwork-config-%s", testResourcePrefix, randomID)
runnerGroupName := fmt.Sprintf("%srunner-group-%s", testResourcePrefix, randomID)

configWithoutNetworkConfiguration := fmt.Sprintf(`
resource "github_organization_network_configuration" "test" {
name = %q
compute_service = "actions"
network_settings_ids = [%q]
}

resource "github_actions_runner_group" "test" {
name = %q
visibility = "all"
}
`, networkConfigurationName, networkSettingsID, runnerGroupName)

configWithNetworkConfiguration := fmt.Sprintf(`
resource "github_organization_network_configuration" "test" {
name = %q
compute_service = "actions"
network_settings_ids = [%q]
}

resource "github_actions_runner_group" "test" {
name = %q
visibility = "all"
network_configuration_id = github_organization_network_configuration.test.id
}
`, networkConfigurationName, networkSettingsID, runnerGroupName)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: configWithoutNetworkConfiguration,
Check: resource.ComposeTestCheckFunc(
Comment thread
austenstone marked this conversation as resolved.
Outdated
resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"),
),
},
{
Config: configWithNetworkConfiguration,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"),
testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: configWithoutNetworkConfiguration,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"),
),
},
},
})
})

t.Run("creates private networking association for hosted runners on create", func(t *testing.T) {
networkSettingsID := testAccOrganizationNetworkConfigurationID(t)
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
resourceName := "github_actions_runner_group.test"
networkConfigurationResourceName := "github_organization_network_configuration.test"
networkConfigurationName := fmt.Sprintf("%snetwork-config-create-%s", testResourcePrefix, randomID)
runnerGroupName := fmt.Sprintf("%srunner-group-create-%s", testResourcePrefix, randomID)

config := fmt.Sprintf(`
resource "github_organization_network_configuration" "test" {
name = %q
compute_service = "actions"
network_settings_ids = [%q]
}

resource "github_actions_runner_group" "test" {
name = %q
visibility = "all"
network_configuration_id = github_organization_network_configuration.test.id
}
`, networkConfigurationName, networkSettingsID, runnerGroupName)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"),
testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
})

t.Run("manages runner visibility", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
repoName := fmt.Sprintf("%srepo-act-runner-%s", testResourcePrefix, randomID)
Expand Down
Loading