From eebea06fc1ecc37e4c7abca9f5229ded09e23538 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 03:01:00 -0400 Subject: [PATCH 01/13] feat: add organization network configuration resource --- github/provider.go | 1 + ...thub_organization_network_configuration.go | 178 ++++++++++++++++++ ...organization_network_configuration_test.go | 124 ++++++++++++ ...zation_network_configuration.html.markdown | 48 +++++ 4 files changed, 351 insertions(+) create mode 100644 github/resource_github_organization_network_configuration.go create mode 100644 github/resource_github_organization_network_configuration_test.go create mode 100644 website/docs/r/organization_network_configuration.html.markdown diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..97c6c4a1e3 100644 --- a/github/provider.go +++ b/github/provider.go @@ -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(), diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go new file mode 100644 index 0000000000..f57345315f --- /dev/null +++ b/github/resource_github_organization_network_configuration.go @@ -0,0 +1,178 @@ +package github + +import ( + "context" + "log" + "net/http" + "regexp" + + "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubOrganizationNetworkConfiguration() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubOrganizationNetworkConfigurationCreate, + Read: resourceGithubOrganizationNetworkConfigurationRead, + Update: resourceGithubOrganizationNetworkConfigurationUpdate, + Delete: resourceGithubOrganizationNetworkConfigurationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.All( + validation.StringLenBetween(1, 100), + validation.StringMatch( + regexp.MustCompile(`^[a-zA-Z0-9._-]+$`), + "name may only contain upper and lowercase letters a-z, numbers 0-9, '.', '-', and '_'", + ), + )), + Description: "Name of the network configuration. Must be between 1 and 100 characters and may only contain upper and lowercase letters a-z, numbers 0-9, '.', '-', and '_'.", + }, + "compute_service": { + Type: schema.TypeString, + Optional: true, + Default: "none", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"none", "actions"}, false)), + Description: "The hosted compute service to use for the network configuration. Can be one of: 'none', 'actions'. Defaults to 'none'.", + }, + "network_settings_ids": { + Type: schema.TypeList, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "An array containing exactly one network settings ID. A network settings resource can only be associated with one network configuration at a time.", + }, + "created_on": { + Type: schema.TypeString, + Computed: true, + Description: "Timestamp when the network configuration was created.", + }, + }, + } +} + +func resourceGithubOrganizationNetworkConfigurationCreate(d *schema.ResourceData, meta any) error { + if err := checkOrganization(meta); err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + ctx := context.Background() + + configuration, _, err := client.Organizations.CreateNetworkConfiguration(ctx, orgName, github.NetworkConfigurationRequest{ + Name: github.Ptr(d.Get("name").(string)), + ComputeService: expandOrganizationNetworkConfigurationComputeService(d.Get("compute_service").(string)), + NetworkSettingsIDs: expandOrganizationNetworkSettingsIDs(d.Get("network_settings_ids").([]any)), + }) + if err != nil { + return err + } + + d.SetId(configuration.GetID()) + + return resourceGithubOrganizationNetworkConfigurationRead(d, meta) +} + +func resourceGithubOrganizationNetworkConfigurationRead(d *schema.ResourceData, meta any) error { + if err := checkOrganization(meta); err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + networkConfigurationID := d.Id() + ctx := context.WithValue(context.Background(), ctxId, networkConfigurationID) + + configuration, resp, err := client.Organizations.GetNetworkConfiguration(ctx, orgName, networkConfigurationID) + if err != nil { + if ghErr, ok := err.(*github.ErrorResponse); ok && ghErr.Response.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Removing organization network configuration %s from state because it no longer exists in GitHub", networkConfigurationID) + d.SetId("") + return nil + } + + return err + } + + if resp != nil && resp.StatusCode == http.StatusNotModified { + return nil + } + + _ = d.Set("name", configuration.GetName()) + if configuration.ComputeService != nil { + _ = d.Set("compute_service", string(*configuration.ComputeService)) + } + _ = d.Set("network_settings_ids", configuration.NetworkSettingsIDs) + if configuration.CreatedOn != nil { + _ = d.Set("created_on", configuration.CreatedOn.Format("2006-01-02T15:04:05Z07:00")) + } + + return nil +} + +func resourceGithubOrganizationNetworkConfigurationUpdate(d *schema.ResourceData, meta any) error { + if err := checkOrganization(meta); err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + networkConfigurationID := d.Id() + ctx := context.WithValue(context.Background(), ctxId, networkConfigurationID) + + _, _, err := client.Organizations.UpdateNetworkConfiguration(ctx, orgName, networkConfigurationID, github.NetworkConfigurationRequest{ + Name: github.Ptr(d.Get("name").(string)), + ComputeService: expandOrganizationNetworkConfigurationComputeService(d.Get("compute_service").(string)), + NetworkSettingsIDs: expandOrganizationNetworkSettingsIDs(d.Get("network_settings_ids").([]any)), + }) + if err != nil { + return err + } + + return resourceGithubOrganizationNetworkConfigurationRead(d, meta) +} + +func resourceGithubOrganizationNetworkConfigurationDelete(d *schema.ResourceData, meta any) error { + if err := checkOrganization(meta); err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + ctx := context.Background() + + _, err := client.Organizations.DeleteNetworkConfigurations(ctx, orgName, d.Id()) + if err != nil { + if ghErr, ok := err.(*github.ErrorResponse); ok && ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + + return err + } + + return nil +} + +func expandOrganizationNetworkConfigurationComputeService(computeService string) *github.ComputeService { + service := github.ComputeService(computeService) + return &service +} + +func expandOrganizationNetworkSettingsIDs(networkSettingsIDs []any) []string { + ids := make([]string, 0, len(networkSettingsIDs)) + for _, networkSettingsID := range networkSettingsIDs { + ids = append(ids, networkSettingsID.(string)) + } + + return ids +} diff --git a/github/resource_github_organization_network_configuration_test.go b/github/resource_github_organization_network_configuration_test.go new file mode 100644 index 0000000000..a83f23b501 --- /dev/null +++ b/github/resource_github_organization_network_configuration_test.go @@ -0,0 +1,124 @@ +package github + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { + t.Run("create", func(t *testing.T) { + networkSettingsID := os.Getenv("GITHUB_TEST_NETWORK_SETTINGS_ID") + if networkSettingsID == "" { + t.Skip("GITHUB_TEST_NETWORK_SETTINGS_ID not set") + } + + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + resourceName := "github_organization_network_configuration.test" + configurationName := fmt.Sprintf("%snetwork-config-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_organization_network_configuration" "test" { + name = %q + compute_service = "actions" + network_settings_ids = [%q] +} +`, configurationName, networkSettingsID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, organization) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", configurationName), + resource.TestCheckResourceAttr(resourceName, "compute_service", "actions"), + resource.TestCheckResourceAttr(resourceName, "network_settings_ids.0", networkSettingsID), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "created_on"), + ), + }, + }, + }) + }) + + t.Run("update", func(t *testing.T) { + networkSettingsID := os.Getenv("GITHUB_TEST_NETWORK_SETTINGS_ID") + if networkSettingsID == "" { + t.Skip("GITHUB_TEST_NETWORK_SETTINGS_ID not set") + } + + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + resourceName := "github_organization_network_configuration.test" + beforeName := fmt.Sprintf("%snetwork-config-%s-a", testResourcePrefix, randomID) + afterName := fmt.Sprintf("%snetwork-config-%s-b", testResourcePrefix, randomID) + + config := ` +resource "github_organization_network_configuration" "test" { + name = %q + compute_service = %q + network_settings_ids = [%q] +} +` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, organization) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, beforeName, "actions", networkSettingsID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", beforeName), + resource.TestCheckResourceAttr(resourceName, "compute_service", "actions"), + resource.TestCheckResourceAttr(resourceName, "network_settings_ids.0", networkSettingsID), + ), + }, + { + Config: fmt.Sprintf(config, afterName, "none", networkSettingsID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", afterName), + resource.TestCheckResourceAttr(resourceName, "compute_service", "none"), + resource.TestCheckResourceAttr(resourceName, "network_settings_ids.0", networkSettingsID), + ), + }, + }, + }) + }) + + t.Run("import", func(t *testing.T) { + networkSettingsID := os.Getenv("GITHUB_TEST_NETWORK_SETTINGS_ID") + if networkSettingsID == "" { + t.Skip("GITHUB_TEST_NETWORK_SETTINGS_ID not set") + } + + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + configurationName := fmt.Sprintf("%snetwork-config-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_organization_network_configuration" "test" { + name = %q + compute_service = "actions" + network_settings_ids = [%q] +} +`, configurationName, networkSettingsID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, organization) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: "github_organization_network_configuration.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) +} diff --git a/website/docs/r/organization_network_configuration.html.markdown b/website/docs/r/organization_network_configuration.html.markdown new file mode 100644 index 0000000000..a327250f2b --- /dev/null +++ b/website/docs/r/organization_network_configuration.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "github" +page_title: "GitHub: github_organization_network_configuration" +description: |- + Creates and manages network configurations for GitHub Actions hosted runners in an organization. +--- + +# github_organization_network_configuration + +This resource allows you to create and manage network configurations for GitHub Actions hosted runners in a GitHub organization. Network configurations enable you to configure networking settings for hosted compute services. + +~> **Note:** This resource is only available for GitHub Enterprise Cloud organizations. See the [GitHub documentation](https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/network-configurations) for more information. + +## Example Usage + +```hcl +resource "github_organization_network_configuration" "example" { + name = "my-network-config" + compute_service = "actions" + network_settings_ids = ["23456789ABCDEF1"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the network configuration. Must be between 1 and 100 characters and may only contain upper and lowercase letters a-z, numbers 0-9, `.`, `-`, and `_`. + +* `compute_service` - (Optional) The hosted compute service to use for the network configuration. Can be one of `none` or `actions`. Defaults to `none`. + +* `network_settings_ids` - (Required) An array containing exactly one network settings ID. Network settings resources are configured separately through your cloud provider. A network settings resource can only be associated with one network configuration at a time. + +## Attributes Reference + +The following additional attributes are exported: + +* `id` - The ID of the network configuration. + +* `created_on` - The timestamp when the network configuration was created. + +## Import + +Organization network configurations can be imported using the network configuration ID: + +```shell +terraform import github_organization_network_configuration.example 1234567890ABCDEF +``` From f562c9b19363af111d7825eca37f1e14d1e263ab Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 03:03:15 -0400 Subject: [PATCH 02/13] refactor: clean up network configuration resource --- ...thub_organization_network_configuration.go | 48 ++++++++------ ...organization_network_configuration_test.go | 64 ++++++++----------- 2 files changed, 55 insertions(+), 57 deletions(-) diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go index f57345315f..0b0d164895 100644 --- a/github/resource_github_organization_network_configuration.go +++ b/github/resource_github_organization_network_configuration.go @@ -11,6 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +var organizationNetworkConfigurationNamePattern = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + func resourceGithubOrganizationNetworkConfiguration() *schema.Resource { return &schema.Resource{ Create: resourceGithubOrganizationNetworkConfigurationCreate, @@ -28,7 +30,7 @@ func resourceGithubOrganizationNetworkConfiguration() *schema.Resource { ValidateDiagFunc: validation.ToDiagFunc(validation.All( validation.StringLenBetween(1, 100), validation.StringMatch( - regexp.MustCompile(`^[a-zA-Z0-9._-]+$`), + organizationNetworkConfigurationNamePattern, "name may only contain upper and lowercase letters a-z, numbers 0-9, '.', '-', and '_'", ), )), @@ -69,11 +71,7 @@ func resourceGithubOrganizationNetworkConfigurationCreate(d *schema.ResourceData orgName := meta.(*Owner).name ctx := context.Background() - configuration, _, err := client.Organizations.CreateNetworkConfiguration(ctx, orgName, github.NetworkConfigurationRequest{ - Name: github.Ptr(d.Get("name").(string)), - ComputeService: expandOrganizationNetworkConfigurationComputeService(d.Get("compute_service").(string)), - NetworkSettingsIDs: expandOrganizationNetworkSettingsIDs(d.Get("network_settings_ids").([]any)), - }) + configuration, _, err := client.Organizations.CreateNetworkConfiguration(ctx, orgName, buildOrganizationNetworkConfigurationRequest(d)) if err != nil { return err } @@ -108,16 +106,7 @@ func resourceGithubOrganizationNetworkConfigurationRead(d *schema.ResourceData, return nil } - _ = d.Set("name", configuration.GetName()) - if configuration.ComputeService != nil { - _ = d.Set("compute_service", string(*configuration.ComputeService)) - } - _ = d.Set("network_settings_ids", configuration.NetworkSettingsIDs) - if configuration.CreatedOn != nil { - _ = d.Set("created_on", configuration.CreatedOn.Format("2006-01-02T15:04:05Z07:00")) - } - - return nil + return setOrganizationNetworkConfigurationState(d, configuration) } func resourceGithubOrganizationNetworkConfigurationUpdate(d *schema.ResourceData, meta any) error { @@ -130,11 +119,7 @@ func resourceGithubOrganizationNetworkConfigurationUpdate(d *schema.ResourceData networkConfigurationID := d.Id() ctx := context.WithValue(context.Background(), ctxId, networkConfigurationID) - _, _, err := client.Organizations.UpdateNetworkConfiguration(ctx, orgName, networkConfigurationID, github.NetworkConfigurationRequest{ - Name: github.Ptr(d.Get("name").(string)), - ComputeService: expandOrganizationNetworkConfigurationComputeService(d.Get("compute_service").(string)), - NetworkSettingsIDs: expandOrganizationNetworkSettingsIDs(d.Get("network_settings_ids").([]any)), - }) + _, _, err := client.Organizations.UpdateNetworkConfiguration(ctx, orgName, networkConfigurationID, buildOrganizationNetworkConfigurationRequest(d)) if err != nil { return err } @@ -176,3 +161,24 @@ func expandOrganizationNetworkSettingsIDs(networkSettingsIDs []any) []string { return ids } + +func buildOrganizationNetworkConfigurationRequest(d *schema.ResourceData) github.NetworkConfigurationRequest { + return github.NetworkConfigurationRequest{ + Name: github.Ptr(d.Get("name").(string)), + ComputeService: expandOrganizationNetworkConfigurationComputeService(d.Get("compute_service").(string)), + NetworkSettingsIDs: expandOrganizationNetworkSettingsIDs(d.Get("network_settings_ids").([]any)), + } +} + +func setOrganizationNetworkConfigurationState(d *schema.ResourceData, configuration *github.NetworkConfiguration) error { + _ = d.Set("name", configuration.GetName()) + if configuration.ComputeService != nil { + _ = d.Set("compute_service", string(*configuration.ComputeService)) + } + _ = d.Set("network_settings_ids", configuration.NetworkSettingsIDs) + if configuration.CreatedOn != nil { + _ = d.Set("created_on", configuration.CreatedOn.Format("2006-01-02T15:04:05Z07:00")) + } + + return nil +} diff --git a/github/resource_github_organization_network_configuration_test.go b/github/resource_github_organization_network_configuration_test.go index a83f23b501..1e6698c121 100644 --- a/github/resource_github_organization_network_configuration_test.go +++ b/github/resource_github_organization_network_configuration_test.go @@ -11,22 +11,13 @@ import ( func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { t.Run("create", func(t *testing.T) { - networkSettingsID := os.Getenv("GITHUB_TEST_NETWORK_SETTINGS_ID") - if networkSettingsID == "" { - t.Skip("GITHUB_TEST_NETWORK_SETTINGS_ID not set") - } + networkSettingsID := testAccOrganizationNetworkConfigurationID(t) randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) resourceName := "github_organization_network_configuration.test" configurationName := fmt.Sprintf("%snetwork-config-%s", testResourcePrefix, randomID) - config := fmt.Sprintf(` -resource "github_organization_network_configuration" "test" { - name = %q - compute_service = "actions" - network_settings_ids = [%q] -} -`, configurationName, networkSettingsID) + config := testAccOrganizationNetworkConfigurationConfig(configurationName, "actions", networkSettingsID) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessMode(t, organization) }, @@ -47,30 +38,19 @@ resource "github_organization_network_configuration" "test" { }) t.Run("update", func(t *testing.T) { - networkSettingsID := os.Getenv("GITHUB_TEST_NETWORK_SETTINGS_ID") - if networkSettingsID == "" { - t.Skip("GITHUB_TEST_NETWORK_SETTINGS_ID not set") - } + networkSettingsID := testAccOrganizationNetworkConfigurationID(t) randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) resourceName := "github_organization_network_configuration.test" beforeName := fmt.Sprintf("%snetwork-config-%s-a", testResourcePrefix, randomID) afterName := fmt.Sprintf("%snetwork-config-%s-b", testResourcePrefix, randomID) - config := ` -resource "github_organization_network_configuration" "test" { - name = %q - compute_service = %q - network_settings_ids = [%q] -} -` - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessMode(t, organization) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(config, beforeName, "actions", networkSettingsID), + Config: testAccOrganizationNetworkConfigurationConfig(beforeName, "actions", networkSettingsID), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", beforeName), resource.TestCheckResourceAttr(resourceName, "compute_service", "actions"), @@ -78,7 +58,7 @@ resource "github_organization_network_configuration" "test" { ), }, { - Config: fmt.Sprintf(config, afterName, "none", networkSettingsID), + Config: testAccOrganizationNetworkConfigurationConfig(afterName, "none", networkSettingsID), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", afterName), resource.TestCheckResourceAttr(resourceName, "compute_service", "none"), @@ -90,21 +70,12 @@ resource "github_organization_network_configuration" "test" { }) t.Run("import", func(t *testing.T) { - networkSettingsID := os.Getenv("GITHUB_TEST_NETWORK_SETTINGS_ID") - if networkSettingsID == "" { - t.Skip("GITHUB_TEST_NETWORK_SETTINGS_ID not set") - } + networkSettingsID := testAccOrganizationNetworkConfigurationID(t) randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) configurationName := fmt.Sprintf("%snetwork-config-%s", testResourcePrefix, randomID) - config := fmt.Sprintf(` -resource "github_organization_network_configuration" "test" { - name = %q - compute_service = "actions" - network_settings_ids = [%q] -} -`, configurationName, networkSettingsID) + config := testAccOrganizationNetworkConfigurationConfig(configurationName, "actions", networkSettingsID) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessMode(t, organization) }, @@ -122,3 +93,24 @@ resource "github_organization_network_configuration" "test" { }) }) } + +func testAccOrganizationNetworkConfigurationID(t *testing.T) string { + t.Helper() + + networkSettingsID := os.Getenv("GITHUB_TEST_NETWORK_SETTINGS_ID") + if networkSettingsID == "" { + t.Skip("GITHUB_TEST_NETWORK_SETTINGS_ID not set") + } + + return networkSettingsID +} + +func testAccOrganizationNetworkConfigurationConfig(name, computeService, networkSettingsID string) string { + return fmt.Sprintf(` +resource "github_organization_network_configuration" "test" { + name = %q + compute_service = %q + network_settings_ids = [%q] +} +`, name, computeService, networkSettingsID) +} From 290b60474be64f904cf7e75a4f98e976b3cc27c2 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 03:13:56 -0400 Subject: [PATCH 03/13] test: align network configuration resource with repo patterns --- ..._github_organization_network_configuration.go | 16 ++++++++++++---- ...ub_organization_network_configuration_test.go | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go index 0b0d164895..14bc104f60 100644 --- a/github/resource_github_organization_network_configuration.go +++ b/github/resource_github_organization_network_configuration.go @@ -171,13 +171,21 @@ func buildOrganizationNetworkConfigurationRequest(d *schema.ResourceData) github } func setOrganizationNetworkConfigurationState(d *schema.ResourceData, configuration *github.NetworkConfiguration) error { - _ = d.Set("name", configuration.GetName()) + if err := d.Set("name", configuration.GetName()); err != nil { + return err + } if configuration.ComputeService != nil { - _ = d.Set("compute_service", string(*configuration.ComputeService)) + if err := d.Set("compute_service", string(*configuration.ComputeService)); err != nil { + return err + } + } + if err := d.Set("network_settings_ids", configuration.NetworkSettingsIDs); err != nil { + return err } - _ = d.Set("network_settings_ids", configuration.NetworkSettingsIDs) if configuration.CreatedOn != nil { - _ = d.Set("created_on", configuration.CreatedOn.Format("2006-01-02T15:04:05Z07:00")) + if err := d.Set("created_on", configuration.CreatedOn.Format("2006-01-02T15:04:05Z07:00")); err != nil { + return err + } } return nil diff --git a/github/resource_github_organization_network_configuration_test.go b/github/resource_github_organization_network_configuration_test.go index 1e6698c121..31686bd0a0 100644 --- a/github/resource_github_organization_network_configuration_test.go +++ b/github/resource_github_organization_network_configuration_test.go @@ -20,7 +20,7 @@ func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { config := testAccOrganizationNetworkConfigurationConfig(configurationName, "actions", networkSettingsID) resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessMode(t, organization) }, + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { @@ -46,7 +46,7 @@ func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { afterName := fmt.Sprintf("%snetwork-config-%s-b", testResourcePrefix, randomID) resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessMode(t, organization) }, + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { @@ -78,7 +78,7 @@ func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { config := testAccOrganizationNetworkConfigurationConfig(configurationName, "actions", networkSettingsID) resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessMode(t, organization) }, + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { From 6d6b5982dfeced02fb675ec6bcd2b0b0ce8adbd8 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 03:15:35 -0400 Subject: [PATCH 04/13] docs: polish organization network configuration resource --- ...anization_network_configuration.html.markdown | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/website/docs/r/organization_network_configuration.html.markdown b/website/docs/r/organization_network_configuration.html.markdown index a327250f2b..f1a77579e9 100644 --- a/website/docs/r/organization_network_configuration.html.markdown +++ b/website/docs/r/organization_network_configuration.html.markdown @@ -2,14 +2,14 @@ layout: "github" page_title: "GitHub: github_organization_network_configuration" description: |- - Creates and manages network configurations for GitHub Actions hosted runners in an organization. + Creates and manages hosted compute network configurations for a GitHub organization. --- # github_organization_network_configuration -This resource allows you to create and manage network configurations for GitHub Actions hosted runners in a GitHub organization. Network configurations enable you to configure networking settings for hosted compute services. +This resource allows you to create and manage hosted compute network configurations for a GitHub Organization. Network configurations allow GitHub-hosted compute services, such as Actions hosted runners, to connect to your private network resources. -~> **Note:** This resource is only available for GitHub Enterprise Cloud organizations. See the [GitHub documentation](https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/network-configurations) for more information. +~> **Note:** This resource is organization-only and is available for GitHub Enterprise Cloud organizations. See the [GitHub documentation](https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/network-configurations) for more information. ## Example Usage @@ -33,12 +33,18 @@ The following arguments are supported: ## Attributes Reference -The following additional attributes are exported: +In addition to the arguments above, the following attributes are exported: * `id` - The ID of the network configuration. * `created_on` - The timestamp when the network configuration was created. +## Notes + +* This resource can only be used with organization accounts. +* GitHub currently allows exactly one `network_settings_ids` value per organization network configuration. +* The `network_settings_ids` value must reference an existing hosted compute network settings resource configured outside this provider. + ## Import Organization network configurations can be imported using the network configuration ID: @@ -46,3 +52,5 @@ Organization network configurations can be imported using the network configurat ```shell terraform import github_organization_network_configuration.example 1234567890ABCDEF ``` + +The network configuration ID can be found using the [list hosted compute network configurations for an organization](https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/network-configurations#list-hosted-compute-network-configurations-for-an-organization) API. From 4f00048a39652a07d20e60776944dad2c11c9de2 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 03:16:36 -0400 Subject: [PATCH 05/13] refactor: use RFC3339 constant for network configuration timestamps --- github/resource_github_organization_network_configuration.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go index 14bc104f60..c803c2b565 100644 --- a/github/resource_github_organization_network_configuration.go +++ b/github/resource_github_organization_network_configuration.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "regexp" + "time" "github.com/google/go-github/v83/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -183,7 +184,7 @@ func setOrganizationNetworkConfigurationState(d *schema.ResourceData, configurat return err } if configuration.CreatedOn != nil { - if err := d.Set("created_on", configuration.CreatedOn.Format("2006-01-02T15:04:05Z07:00")); err != nil { + if err := d.Set("created_on", configuration.CreatedOn.Format(time.RFC3339)); err != nil { return err } } From e487e3514a0fd88d5703cd762e60248eef35b2a2 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 08:07:48 -0400 Subject: [PATCH 06/13] refactor: address network configuration review comments --- ...thub_organization_network_configuration.go | 123 +++++++++++------- ...organization_network_configuration_test.go | 37 +++--- 2 files changed, 94 insertions(+), 66 deletions(-) diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go index c803c2b565..ca2c0b3161 100644 --- a/github/resource_github_organization_network_configuration.go +++ b/github/resource_github_organization_network_configuration.go @@ -2,12 +2,13 @@ package github import ( "context" - "log" "net/http" "regexp" "time" "github.com/google/go-github/v83/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -16,10 +17,11 @@ var organizationNetworkConfigurationNamePattern = regexp.MustCompile(`^[a-zA-Z0- func resourceGithubOrganizationNetworkConfiguration() *schema.Resource { return &schema.Resource{ - Create: resourceGithubOrganizationNetworkConfigurationCreate, - Read: resourceGithubOrganizationNetworkConfigurationRead, - Update: resourceGithubOrganizationNetworkConfigurationUpdate, - Delete: resourceGithubOrganizationNetworkConfigurationDelete, + Description: "This resource allows you to create and manage hosted compute network configurations for a GitHub organization.", + CreateContext: resourceGithubOrganizationNetworkConfigurationCreate, + ReadContext: resourceGithubOrganizationNetworkConfigurationRead, + UpdateContext: resourceGithubOrganizationNetworkConfigurationUpdate, + DeleteContext: resourceGithubOrganizationNetworkConfigurationDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -63,114 +65,137 @@ func resourceGithubOrganizationNetworkConfiguration() *schema.Resource { } } -func resourceGithubOrganizationNetworkConfigurationCreate(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationNetworkConfigurationCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "organization", meta.(*Owner).name) + if err := checkOrganization(meta); err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.Background() + computeService := github.ComputeService(d.Get("compute_service").(string)) + networkSettingsIDs := []string{d.Get("network_settings_ids").([]any)[0].(string)} + + tflog.Debug(ctx, "Creating organization network configuration", map[string]any{ + "name": d.Get("name").(string), + "compute_service": d.Get("compute_service").(string), + "network_settings_ids": networkSettingsIDs, + }) - configuration, _, err := client.Organizations.CreateNetworkConfiguration(ctx, orgName, buildOrganizationNetworkConfigurationRequest(d)) + configuration, _, err := client.Organizations.CreateNetworkConfiguration(ctx, orgName, github.NetworkConfigurationRequest{ + Name: github.Ptr(d.Get("name").(string)), + ComputeService: &computeService, + NetworkSettingsIDs: networkSettingsIDs, + }) if err != nil { - return err + return diag.FromErr(err) } d.SetId(configuration.GetID()) + if err := setOrganizationNetworkConfigurationState(d, configuration); err != nil { + return diag.FromErr(err) + } - return resourceGithubOrganizationNetworkConfigurationRead(d, meta) + return nil } -func resourceGithubOrganizationNetworkConfigurationRead(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationNetworkConfigurationRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "id", d.Id()) + ctx = tflog.SetField(ctx, "organization", meta.(*Owner).name) + if err := checkOrganization(meta); err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client orgName := meta.(*Owner).name networkConfigurationID := d.Id() - ctx := context.WithValue(context.Background(), ctxId, networkConfigurationID) + ctx = context.WithValue(ctx, ctxId, networkConfigurationID) configuration, resp, err := client.Organizations.GetNetworkConfiguration(ctx, orgName, networkConfigurationID) if err != nil { if ghErr, ok := err.(*github.ErrorResponse); ok && ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[WARN] Removing organization network configuration %s from state because it no longer exists in GitHub", networkConfigurationID) + tflog.Info(ctx, "Organization network configuration not found, removing from state", map[string]any{"id": networkConfigurationID}) d.SetId("") return nil } - return err + return diag.FromErr(err) } if resp != nil && resp.StatusCode == http.StatusNotModified { return nil } - return setOrganizationNetworkConfigurationState(d, configuration) + if err := setOrganizationNetworkConfigurationState(d, configuration); err != nil { + return diag.FromErr(err) + } + + return nil } -func resourceGithubOrganizationNetworkConfigurationUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationNetworkConfigurationUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "id", d.Id()) + ctx = tflog.SetField(ctx, "organization", meta.(*Owner).name) + if err := checkOrganization(meta); err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client orgName := meta.(*Owner).name networkConfigurationID := d.Id() - ctx := context.WithValue(context.Background(), ctxId, networkConfigurationID) + ctx = context.WithValue(ctx, ctxId, networkConfigurationID) + computeService := github.ComputeService(d.Get("compute_service").(string)) + networkSettingsIDs := []string{d.Get("network_settings_ids").([]any)[0].(string)} - _, _, err := client.Organizations.UpdateNetworkConfiguration(ctx, orgName, networkConfigurationID, buildOrganizationNetworkConfigurationRequest(d)) + tflog.Debug(ctx, "Updating organization network configuration", map[string]any{ + "name": d.Get("name").(string), + "compute_service": d.Get("compute_service").(string), + "network_settings_ids": networkSettingsIDs, + }) + + configuration, _, err := client.Organizations.UpdateNetworkConfiguration(ctx, orgName, networkConfigurationID, github.NetworkConfigurationRequest{ + Name: github.Ptr(d.Get("name").(string)), + ComputeService: &computeService, + NetworkSettingsIDs: networkSettingsIDs, + }) if err != nil { - return err + return diag.FromErr(err) + } + + if err := setOrganizationNetworkConfigurationState(d, configuration); err != nil { + return diag.FromErr(err) } - return resourceGithubOrganizationNetworkConfigurationRead(d, meta) + return nil } -func resourceGithubOrganizationNetworkConfigurationDelete(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationNetworkConfigurationDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + ctx = tflog.SetField(ctx, "id", d.Id()) + ctx = tflog.SetField(ctx, "organization", meta.(*Owner).name) + if err := checkOrganization(meta); err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.Background() + tflog.Debug(ctx, "Deleting organization network configuration") _, err := client.Organizations.DeleteNetworkConfigurations(ctx, orgName, d.Id()) if err != nil { if ghErr, ok := err.(*github.ErrorResponse); ok && ghErr.Response.StatusCode == http.StatusNotFound { return nil } - return err + return diag.FromErr(err) } return nil } -func expandOrganizationNetworkConfigurationComputeService(computeService string) *github.ComputeService { - service := github.ComputeService(computeService) - return &service -} - -func expandOrganizationNetworkSettingsIDs(networkSettingsIDs []any) []string { - ids := make([]string, 0, len(networkSettingsIDs)) - for _, networkSettingsID := range networkSettingsIDs { - ids = append(ids, networkSettingsID.(string)) - } - - return ids -} - -func buildOrganizationNetworkConfigurationRequest(d *schema.ResourceData) github.NetworkConfigurationRequest { - return github.NetworkConfigurationRequest{ - Name: github.Ptr(d.Get("name").(string)), - ComputeService: expandOrganizationNetworkConfigurationComputeService(d.Get("compute_service").(string)), - NetworkSettingsIDs: expandOrganizationNetworkSettingsIDs(d.Get("network_settings_ids").([]any)), - } -} - func setOrganizationNetworkConfigurationState(d *schema.ResourceData, configuration *github.NetworkConfiguration) error { if err := d.Set("name", configuration.GetName()); err != nil { return err diff --git a/github/resource_github_organization_network_configuration_test.go b/github/resource_github_organization_network_configuration_test.go index 31686bd0a0..1ead88efe8 100644 --- a/github/resource_github_organization_network_configuration_test.go +++ b/github/resource_github_organization_network_configuration_test.go @@ -7,6 +7,9 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { @@ -25,13 +28,13 @@ func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", configurationName), - resource.TestCheckResourceAttr(resourceName, "compute_service", "actions"), - resource.TestCheckResourceAttr(resourceName, "network_settings_ids.0", networkSettingsID), - resource.TestCheckResourceAttrSet(resourceName, "id"), - resource.TestCheckResourceAttrSet(resourceName, "created_on"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(configurationName)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("compute_service"), knownvalue.StringExact("actions")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("network_settings_ids"), knownvalue.ListExact([]knownvalue.Check{knownvalue.StringExact(networkSettingsID)})), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("created_on"), knownvalue.NotNull()), + }, }, }, }) @@ -51,19 +54,19 @@ func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccOrganizationNetworkConfigurationConfig(beforeName, "actions", networkSettingsID), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", beforeName), - resource.TestCheckResourceAttr(resourceName, "compute_service", "actions"), - resource.TestCheckResourceAttr(resourceName, "network_settings_ids.0", networkSettingsID), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(beforeName)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("compute_service"), knownvalue.StringExact("actions")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("network_settings_ids"), knownvalue.ListExact([]knownvalue.Check{knownvalue.StringExact(networkSettingsID)})), + }, }, { Config: testAccOrganizationNetworkConfigurationConfig(afterName, "none", networkSettingsID), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "name", afterName), - resource.TestCheckResourceAttr(resourceName, "compute_service", "none"), - resource.TestCheckResourceAttr(resourceName, "network_settings_ids.0", networkSettingsID), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(afterName)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("compute_service"), knownvalue.StringExact("none")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("network_settings_ids"), knownvalue.ListExact([]knownvalue.Check{knownvalue.StringExact(networkSettingsID)})), + }, }, }, }) From e8b240be75048d3824d8cf539058d7450bb5b63c Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 08:11:43 -0400 Subject: [PATCH 07/13] refactor: align network configuration with project patterns --- .../resource_github_organization_network_configuration.go | 7 +++++-- ...ource_github_organization_network_configuration_test.go | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go index ca2c0b3161..5533bfce8a 100644 --- a/github/resource_github_organization_network_configuration.go +++ b/github/resource_github_organization_network_configuration.go @@ -2,6 +2,7 @@ package github import ( "context" + "errors" "net/http" "regexp" "time" @@ -115,7 +116,8 @@ func resourceGithubOrganizationNetworkConfigurationRead(ctx context.Context, d * configuration, resp, err := client.Organizations.GetNetworkConfiguration(ctx, orgName, networkConfigurationID) if err != nil { - if ghErr, ok := err.(*github.ErrorResponse); ok && ghErr.Response.StatusCode == http.StatusNotFound { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { tflog.Info(ctx, "Organization network configuration not found, removing from state", map[string]any{"id": networkConfigurationID}) d.SetId("") return nil @@ -186,7 +188,8 @@ func resourceGithubOrganizationNetworkConfigurationDelete(ctx context.Context, d tflog.Debug(ctx, "Deleting organization network configuration") _, err := client.Organizations.DeleteNetworkConfigurations(ctx, orgName, d.Id()) if err != nil { - if ghErr, ok := err.(*github.ErrorResponse); ok && ghErr.Response.StatusCode == http.StatusNotFound { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { return nil } diff --git a/github/resource_github_organization_network_configuration_test.go b/github/resource_github_organization_network_configuration_test.go index 1ead88efe8..a1fc6ae931 100644 --- a/github/resource_github_organization_network_configuration_test.go +++ b/github/resource_github_organization_network_configuration_test.go @@ -86,6 +86,9 @@ func TestAccGithubOrganizationNetworkConfiguration(t *testing.T) { Steps: []resource.TestStep{ { Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_network_configuration.test", tfjsonpath.New("id"), knownvalue.NotNull()), + }, }, { ResourceName: "github_organization_network_configuration.test", From 4d3ceb3f78b7a29e4207b680d0457a17134b0916 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 08:49:06 -0400 Subject: [PATCH 08/13] feat: add link for github_organization_network_configuration in documentation --- website/github.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/github.erb b/website/github.erb index 997536b42f..15439e0854 100644 --- a/website/github.erb +++ b/website/github.erb @@ -352,6 +352,9 @@
  • organization_role_user
  • +
  • + github_organization_network_configuration +
  • github_organization_ruleset
  • From 7d1303dcc8d0f56f645e66d4ff3adba837013996 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 09:54:03 -0400 Subject: [PATCH 09/13] refactor: enhance error handling and documentation for organization network configurations --- ...ce_github_organization_network_configuration.go | 14 ++++++++++++-- ...rganization_network_configuration.html.markdown | 5 ++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go index 5533bfce8a..a62dcd8bc0 100644 --- a/github/resource_github_organization_network_configuration.go +++ b/github/resource_github_organization_network_configuration.go @@ -3,6 +3,7 @@ package github import ( "context" "errors" + "fmt" "net/http" "regexp" "time" @@ -90,7 +91,7 @@ func resourceGithubOrganizationNetworkConfigurationCreate(ctx context.Context, d NetworkSettingsIDs: networkSettingsIDs, }) if err != nil { - return diag.FromErr(err) + return organizationNetworkConfigurationDiagnostics(err) } d.SetId(configuration.GetID()) @@ -164,7 +165,7 @@ func resourceGithubOrganizationNetworkConfigurationUpdate(ctx context.Context, d NetworkSettingsIDs: networkSettingsIDs, }) if err != nil { - return diag.FromErr(err) + return organizationNetworkConfigurationDiagnostics(err) } if err := setOrganizationNetworkConfigurationState(d, configuration); err != nil { @@ -219,3 +220,12 @@ func setOrganizationNetworkConfigurationState(d *schema.ResourceData, configurat return nil } + +func organizationNetworkConfigurationDiagnostics(err error) diag.Diagnostics { + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == http.StatusUnprocessableEntity { + return diag.FromErr(fmt.Errorf("%w. if you are using Azure private networking, ensure the provided network settings GitHubId matches the organization scope; organization-level configurations may fail when the backing GitHub.Network/networkSettings resource was created with an enterprise databaseId", err)) + } + + return diag.FromErr(err) +} diff --git a/website/docs/r/organization_network_configuration.html.markdown b/website/docs/r/organization_network_configuration.html.markdown index f1a77579e9..7ae18d555a 100644 --- a/website/docs/r/organization_network_configuration.html.markdown +++ b/website/docs/r/organization_network_configuration.html.markdown @@ -11,6 +11,8 @@ This resource allows you to create and manage hosted compute network configurati ~> **Note:** This resource is organization-only and is available for GitHub Enterprise Cloud organizations. See the [GitHub documentation](https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/network-configurations) for more information. +~> **Note:** Organization-level network configurations are only available when enterprise policy allows organizations to create their own hosted compute network configurations. Otherwise, organizations can only inherit enterprise-level network configurations. + ## Example Usage ```hcl @@ -29,7 +31,7 @@ The following arguments are supported: * `compute_service` - (Optional) The hosted compute service to use for the network configuration. Can be one of `none` or `actions`. Defaults to `none`. -* `network_settings_ids` - (Required) An array containing exactly one network settings ID. Network settings resources are configured separately through your cloud provider. A network settings resource can only be associated with one network configuration at a time. +* `network_settings_ids` - (Required) An array containing exactly one network settings ID. Network settings resources are configured separately through your cloud provider. For Azure private networking, use the `GitHubId` returned by the Azure `GitHub.Network/networkSettings` resource, not the Azure ARM resource ID. A network settings resource can only be associated with one network configuration at a time. ## Attributes Reference @@ -44,6 +46,7 @@ In addition to the arguments above, the following attributes are exported: * This resource can only be used with organization accounts. * GitHub currently allows exactly one `network_settings_ids` value per organization network configuration. * The `network_settings_ids` value must reference an existing hosted compute network settings resource configured outside this provider. +* For organization-scoped configurations backed by Azure private networking, create the Azure `GitHub.Network/networkSettings` resource using the GitHub organization's `databaseId`. Using a mismatched scope, such as an enterprise `databaseId` for an organization configuration, can cause GitHub to reject the configuration. ## Import From 44bdafcb7e24a7e3ef8f4cbe90df290bf7958f74 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 10:16:06 -0400 Subject: [PATCH 10/13] feat: add support for network configurations in GitHub Actions runner groups --- .../resource_github_actions_runner_group.go | 105 ++++++++++++++++-- ...source_github_actions_runner_group_test.go | 65 +++++++++++ .../r/actions_hosted_runner.html.markdown | 3 + .../docs/r/actions_runner_group.html.markdown | 20 ++++ 4 files changed, 182 insertions(+), 11 deletions(-) diff --git a/github/resource_github_actions_runner_group.go b/github/resource_github_actions_runner_group.go index eb4c85a495..3de0eb9b15 100644 --- a/github/resource_github_actions_runner_group.go +++ b/github/resource_github_actions_runner_group.go @@ -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, @@ -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 + 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 { @@ -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 { + return err } } - return runnerGroup, resp, err + + return resourceGithubActionsRunnerGroupRead(d, meta) } func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) error { @@ -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, @@ -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{} diff --git a/github/resource_github_actions_runner_group_test.go b/github/resource_github_actions_runner_group_test.go index a399e5f140..3817a913a7 100644 --- a/github/resource_github_actions_runner_group_test.go +++ b/github/resource_github_actions_runner_group_test.go @@ -97,6 +97,71 @@ 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" + 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( + resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"), + ), + }, + { + Config: configWithNetworkConfiguration, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: configWithoutNetworkConfiguration, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"), + ), + }, + }, + }) + }) + t.Run("manages runner visibility", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%srepo-act-runner-%s", testResourcePrefix, randomID) diff --git a/website/docs/r/actions_hosted_runner.html.markdown b/website/docs/r/actions_hosted_runner.html.markdown index a78b0188f7..597300b700 100644 --- a/website/docs/r/actions_hosted_runner.html.markdown +++ b/website/docs/r/actions_hosted_runner.html.markdown @@ -72,6 +72,8 @@ The following arguments are supported: * `public_ip_enabled` - (Optional) Whether to enable static public IP for the runner. Note there are account limits. To list limits, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/limits`. Defaults to false. * `image_version` - (Optional) The version of the runner image to deploy. This is only relevant for runners using custom images. +~> **Note:** GitHub private networking for GitHub-hosted runners is configured on the runner group, not directly on the hosted runner. To attach a hosted runner to private networking, associate the runner group with a `github_organization_network_configuration` via `github_actions_runner_group.network_configuration_id`, then place the hosted runner in that group. + ## Timeouts The `timeouts` block allows you to specify timeouts for certain actions: @@ -135,6 +137,7 @@ $ terraform import github_actions_hosted_runner.example 123456 * Deletion of hosted runners is asynchronous. The provider will poll for up to 10 minutes (configurable via timeouts) to confirm deletion. * Runner creation and updates may take several minutes as GitHub provisions the infrastructure. * Static public IPs are subject to account limits. Check your organization's limits before enabling. +* `public_ip_enabled` controls static public IP allocation and is separate from GitHub private networking. ## Getting Available Images and Sizes diff --git a/website/docs/r/actions_runner_group.html.markdown b/website/docs/r/actions_runner_group.html.markdown index 2c0c02fe84..2540ed732d 100644 --- a/website/docs/r/actions_runner_group.html.markdown +++ b/website/docs/r/actions_runner_group.html.markdown @@ -22,6 +22,18 @@ resource "github_actions_runner_group" "example" { visibility = "selected" selected_repository_ids = [github_repository.example.repo_id] } + +resource "github_organization_network_configuration" "private_network" { + name = "private-network" + compute_service = "actions" + network_settings_ids = ["123456789ABCDEF"] +} + +resource "github_actions_runner_group" "private_networked" { + name = "private-networked-runners" + visibility = "all" + network_configuration_id = github_organization_network_configuration.private_network.id +} ``` ## Argument Reference @@ -34,6 +46,7 @@ The following arguments are supported: * `selected_workflows` - (Optional) List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to true. * `visibility` - (Optional) Visibility of a runner group. Whether the runner group can include `all`, `selected`, or `private` repositories. A value of `private` is not currently supported due to limitations in the GitHub API. * `allows_public_repositories` - (Optional) Whether public repositories can be added to the runner group. Defaults to false. +* `network_configuration_id` - (Optional) The ID of a hosted compute network configuration to associate with this runner group. This is the GitHub-side linkage used for GitHub-hosted private networking. ## Attributes Reference @@ -47,6 +60,13 @@ The following arguments are supported: * `visibility` - The visibility of the runner group * `restricted_to_workflows` - If true, the runner group will be restricted to running only the workflows specified in the selected_workflows array. Defaults to false. * `selected_workflows` - 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` - The ID of the hosted compute network configuration associated with this runner group + +## Private networking + +GitHub private networking for GitHub-hosted runners is attached through the runner group, not directly on the hosted runner. + +Use `github_organization_network_configuration` to manage the hosted compute network configuration, then set `network_configuration_id` on `github_actions_runner_group` so any `github_actions_hosted_runner` placed in that group uses the private networking association. ## Import From 3aebd44e6e671173a8959c717cd44943f44400fb Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 10:19:41 -0400 Subject: [PATCH 11/13] feat: add network configuration validation for GitHub Actions runner groups --- ...source_github_actions_runner_group_test.go | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/github/resource_github_actions_runner_group_test.go b/github/resource_github_actions_runner_group_test.go index 3817a913a7..e793f9e5ac 100644 --- a/github/resource_github_actions_runner_group_test.go +++ b/github/resource_github_actions_runner_group_test.go @@ -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) @@ -101,6 +124,7 @@ func TestAccGithubActionsRunnerGroup(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) @@ -145,6 +169,7 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) { Config: configWithNetworkConfiguration, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"), + testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName), ), }, { @@ -162,6 +187,48 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) { }) }) + 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) From 6127464bf5c66a658c38ce613a61512e4ddb7f21 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 10:25:26 -0400 Subject: [PATCH 12/13] feat: update advanced hosted runner configuration and enhance documentation for network settings --- examples/hosted_runner/main.tf | 8 ++++---- website/docs/r/actions_runner_group.html.markdown | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/hosted_runner/main.tf b/examples/hosted_runner/main.tf index 0f1838cb40..4c1a46115d 100644 --- a/examples/hosted_runner/main.tf +++ b/examples/hosted_runner/main.tf @@ -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 } diff --git a/website/docs/r/actions_runner_group.html.markdown b/website/docs/r/actions_runner_group.html.markdown index 2540ed732d..2a4508a6ac 100644 --- a/website/docs/r/actions_runner_group.html.markdown +++ b/website/docs/r/actions_runner_group.html.markdown @@ -68,6 +68,8 @@ GitHub private networking for GitHub-hosted runners is attached through the runn Use `github_organization_network_configuration` to manage the hosted compute network configuration, then set `network_configuration_id` on `github_actions_runner_group` so any `github_actions_hosted_runner` placed in that group uses the private networking association. +For Azure private networking, `network_configuration_id` should reference the GitHub organization network configuration ID, not the Azure ARM resource ID for `GitHub.Network/networkSettings`. + ## Import This resource can be imported using the ID of the runner group: From ea9816bdc022716058ca69b7840d76a9350c078c Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Sun, 15 Mar 2026 11:50:17 -0400 Subject: [PATCH 13/13] refactor: align runner group networking with review feedback --- .../resource_github_actions_runner_group.go | 136 ++++++++---------- ...source_github_actions_runner_group_test.go | 60 ++++---- ...thub_organization_network_configuration.go | 4 +- 3 files changed, 87 insertions(+), 113 deletions(-) diff --git a/github/resource_github_actions_runner_group.go b/github/resource_github_actions_runner_group.go index 3de0eb9b15..9bdf3dfd35 100644 --- a/github/resource_github_actions_runner_group.go +++ b/github/resource_github_actions_runner_group.go @@ -125,7 +125,7 @@ func getOrganizationRunnerGroup(client *github.Client, ctx context.Context, org runnerGroup, resp, err := client.Actions.GetOrganizationRunnerGroup(ctx, org, groupID) if err != nil { var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { + if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == http.StatusNotModified { // ignore error StatusNotModified return runnerGroup, resp, nil } @@ -164,6 +164,47 @@ func setGithubActionsRunnerGroupNetworkingState(d *schema.ResourceData, runnerGr return nil } +func setGithubActionsRunnerGroupState(d *schema.ResourceData, runnerGroup *github.RunnerGroup, etag string, selectedRepositoryIDs []int64) error { + if err := d.Set("etag", etag); err != nil { + return err + } + if err := d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()); err != nil { + return err + } + if err := d.Set("default", runnerGroup.GetDefault()); err != nil { + return err + } + if err := d.Set("id", strconv.FormatInt(runnerGroup.GetID(), 10)); err != nil { + return err + } + if err := d.Set("inherited", runnerGroup.GetInherited()); err != nil { + return err + } + if err := d.Set("name", runnerGroup.GetName()); err != nil { + return err + } + if err := d.Set("runners_url", runnerGroup.GetRunnersURL()); err != nil { + return err + } + if err := d.Set("selected_repositories_url", runnerGroup.GetSelectedRepositoriesURL()); err != nil { + return err + } + if err := d.Set("visibility", runnerGroup.GetVisibility()); err != nil { + return err + } + if err := d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { + return err + } + if err := d.Set("restricted_to_workflows", runnerGroup.GetRestrictedToWorkflows()); err != nil { + return err + } + if err := d.Set("selected_workflows", runnerGroup.SelectedWorkflows); err != nil { + return err + } + + return nil +} + func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta any) error { err := checkOrganization(meta) if err != nil { @@ -216,52 +257,28 @@ func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta any) er return err } d.SetId(strconv.FormatInt(runnerGroup.GetID(), 10)) - if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { - return err - } - if err = d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()); err != nil { - return err - } - if err = d.Set("default", runnerGroup.GetDefault()); err != nil { - return err - } - - if err = d.Set("id", strconv.FormatInt(runnerGroup.GetID(), 10)); err != nil { - return err - } - if err = d.Set("inherited", runnerGroup.GetInherited()); err != nil { - return err - } - if err = d.Set("name", runnerGroup.GetName()); err != nil { - return err - } - if err = d.Set("runners_url", runnerGroup.GetRunnersURL()); err != nil { - return err - } - if err = d.Set("selected_repositories_url", runnerGroup.GetSelectedRepositoriesURL()); err != nil { - return err - } - if err = d.Set("visibility", runnerGroup.GetVisibility()); err != nil { - return err - } - if err = d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { // Note: runnerGroup has no method to get selected repository IDs - return err - } - if err = d.Set("restricted_to_workflows", runnerGroup.GetRestrictedToWorkflows()); err != nil { - return err - } - if err = d.Set("selected_workflows", runnerGroup.SelectedWorkflows); err != nil { + if err = setGithubActionsRunnerGroupState(d, runnerGroup, resp.Header.Get("ETag"), selectedRepositoryIDs); err != nil { return err } if networkConfigurationID, ok := d.GetOk("network_configuration_id"); ok { networkConfigurationIDValue := networkConfigurationID.(string) + // The create endpoint does not accept network_configuration_id, so private networking + // must be attached with a follow-up PATCH after the runner group has been created. if _, err = updateOrganizationRunnerGroupNetworking(client, ctx, orgName, runnerGroup.GetID(), &networkConfigurationIDValue); err != nil { return err } + + if err = setGithubActionsRunnerGroupNetworkingState(d, &organizationRunnerGroup{NetworkConfigurationID: &networkConfigurationIDValue}); err != nil { + return err + } + } else { + if err = setGithubActionsRunnerGroupNetworkingState(d, nil); err != nil { + return err + } } - return resourceGithubActionsRunnerGroupRead(d, meta) + return nil } func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) error { @@ -286,7 +303,7 @@ func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) erro if err != nil { var ghErr *github.ErrorResponse if errors.As(err, &ghErr) { - if ghErr.Response.StatusCode == http.StatusNotFound { + if ghErr.Response != nil && ghErr.Response.StatusCode == http.StatusNotFound { log.Printf("[INFO] Removing organization runner group %s/%s from state because it no longer exists in GitHub", orgName, d.Id()) d.SetId("") @@ -300,48 +317,12 @@ func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) erro if runnerGroup == nil { return nil } - - if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { - return err - } - if err = d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()); err != nil { - return err - } - if err = d.Set("default", runnerGroup.GetDefault()); err != nil { - return err - } - if err = d.Set("id", strconv.FormatInt(runnerGroup.GetID(), 10)); err != nil { - return err - } - if err = d.Set("inherited", runnerGroup.GetInherited()); err != nil { - return err - } - if err = d.Set("name", runnerGroup.GetName()); err != nil { - return err - } - if err = d.Set("runners_url", runnerGroup.GetRunnersURL()); err != nil { - return err - } - if err = d.Set("selected_repositories_url", runnerGroup.GetSelectedRepositoriesURL()); err != nil { - return err - } - if err = d.Set("visibility", runnerGroup.GetVisibility()); err != nil { - return err - } - if err = d.Set("restricted_to_workflows", runnerGroup.GetRestrictedToWorkflows()); err != nil { - return err - } - if err = d.Set("selected_workflows", runnerGroup.SelectedWorkflows); err != nil { - return err - } + runnerGroupEtag := resp.Header.Get("ETag") 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{ @@ -365,7 +346,10 @@ func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) erro options.Page = resp.NextPage } - if err = d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { + if err = setGithubActionsRunnerGroupState(d, runnerGroup, runnerGroupEtag, selectedRepositoryIDs); err != nil { + return err + } + if err = setGithubActionsRunnerGroupNetworkingState(d, runnerGroupNetworking); err != nil { return err } diff --git a/github/resource_github_actions_runner_group_test.go b/github/resource_github_actions_runner_group_test.go index e793f9e5ac..84a42f7eb2 100644 --- a/github/resource_github_actions_runner_group_test.go +++ b/github/resource_github_actions_runner_group_test.go @@ -4,34 +4,24 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/compare" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) -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 testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName string) statecheck.StateCheck { + return statecheck.CompareValuePairs( + resourceName, + tfjsonpath.New("network_configuration_id"), + networkConfigurationResourceName, + tfjsonpath.New("id"), + compare.ValuesSame(), + ) } func TestAccGithubActionsRunnerGroup(t *testing.T) { @@ -161,16 +151,16 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) { Steps: []resource.TestStep{ { Config: configWithoutNetworkConfiguration, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("network_configuration_id"), knownvalue.Null()), + }, }, { Config: configWithNetworkConfiguration, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("network_configuration_id"), knownvalue.NotNull()), testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName), - ), + }, }, { ResourceName: resourceName, @@ -179,9 +169,9 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) { }, { Config: configWithoutNetworkConfiguration, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("network_configuration_id"), knownvalue.Null()), + }, }, }, }) @@ -215,10 +205,10 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("network_configuration_id"), knownvalue.NotNull()), testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName), - ), + }, }, { ResourceName: resourceName, diff --git a/github/resource_github_organization_network_configuration.go b/github/resource_github_organization_network_configuration.go index a62dcd8bc0..948da3d7dd 100644 --- a/github/resource_github_organization_network_configuration.go +++ b/github/resource_github_organization_network_configuration.go @@ -118,7 +118,7 @@ func resourceGithubOrganizationNetworkConfigurationRead(ctx context.Context, d * configuration, resp, err := client.Organizations.GetNetworkConfiguration(ctx, orgName, networkConfigurationID) if err != nil { var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == http.StatusNotFound { tflog.Info(ctx, "Organization network configuration not found, removing from state", map[string]any{"id": networkConfigurationID}) d.SetId("") return nil @@ -190,7 +190,7 @@ func resourceGithubOrganizationNetworkConfigurationDelete(ctx context.Context, d _, err := client.Organizations.DeleteNetworkConfigurations(ctx, orgName, d.Id()) if err != nil { var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound { + if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == http.StatusNotFound { return nil }