diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..95e1b2b29b 100644 --- a/github/provider.go +++ b/github/provider.go @@ -216,6 +216,7 @@ func Provider() *schema.Provider { "github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(), "github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(), "github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(), + "github_organization_inherited_runner_group_settings": resourceGithubOrganizationInheritedRunnerGroupSettings(), "github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(), "github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(), }, diff --git a/github/resource_github_organization_inherited_runner_group_settings.go b/github/resource_github_organization_inherited_runner_group_settings.go new file mode 100644 index 0000000000..c749e5be4c --- /dev/null +++ b/github/resource_github_organization_inherited_runner_group_settings.go @@ -0,0 +1,383 @@ +package github + +import ( + "context" + "fmt" + "log" + "net/http" + "strconv" + + "github.com/google/go-github/v83/github" + "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" +) + +func resourceGithubOrganizationInheritedRunnerGroupSettings() *schema.Resource { + return &schema.Resource{ + Description: "Manages organization-level settings for an enterprise Actions runner group inherited by the organization.", + + CreateContext: resourceGithubOrganizationInheritedRunnerGroupSettingsCreate, + ReadContext: resourceGithubOrganizationInheritedRunnerGroupSettingsRead, + UpdateContext: resourceGithubOrganizationInheritedRunnerGroupSettingsUpdate, + DeleteContext: resourceGithubOrganizationInheritedRunnerGroupSettingsDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubOrganizationInheritedRunnerGroupSettingsImport, + }, + + Schema: map[string]*schema.Schema{ + "organization": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The GitHub organization name.", + }, + "enterprise_runner_group_name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the enterprise runner group inherited by the organization.", + }, + "runner_group_id": { + Type: schema.TypeInt, + Computed: true, + Description: "The ID of the inherited enterprise runner group in the organization.", + }, + "inherited": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether this runner group is inherited from the enterprise.", + }, + "visibility": { + Type: schema.TypeString, + Optional: true, + Default: "selected", + Description: "The visibility of the runner group. Can be 'all', 'selected', or 'private'.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "selected", "private"}, false)), + }, + "selected_repository_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Set: schema.HashInt, + Optional: true, + Description: "List of repository IDs that can access the runner group. Only applicable when visibility is set to 'selected'.", + }, + "allows_public_repositories": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether public repositories can be added to the runner group.", + }, + "restricted_to_workflows": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If 'true', the runner group will be restricted to running only the workflows specified in the 'selected_workflows' array. Defaults to 'false'.", + }, + "selected_workflows": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + 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'.", + }, + }, + } +} + +func findInheritedEnterpriseRunnerGroupByName(client *github.Client, ctx context.Context, org string, name string) (*github.RunnerGroup, error) { + opts := &github.ListOrgRunnerGroupOptions{ + ListOptions: github.ListOptions{ + PerPage: maxPerPage, + }, + } + + for { + groups, resp, err := client.Actions.ListOrganizationRunnerGroups(ctx, org, opts) + if err != nil { + return nil, err + } + + for _, group := range groups.RunnerGroups { + if group.GetInherited() && group.GetName() == name { + return group, nil + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + + return nil, fmt.Errorf("inherited enterprise runner group '%s' not found in organization '%s'. Ensure the enterprise runner group is shared with this organization", name, org) +} + +func resourceGithubOrganizationInheritedRunnerGroupSettingsCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + org := d.Get("organization").(string) + enterpriseRunnerGroupName := d.Get("enterprise_runner_group_name").(string) + visibility := d.Get("visibility").(string) + allowsPublicRepositories := d.Get("allows_public_repositories").(bool) + restrictedToWorkflows := d.Get("restricted_to_workflows").(bool) + selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids") + + selectedWorkflows := []string{} + if workflows, ok := d.GetOk("selected_workflows"); ok { + for _, workflow := range workflows.([]any) { + selectedWorkflows = append(selectedWorkflows, workflow.(string)) + } + } + + // Find the inherited enterprise runner group by name + runnerGroup, err := findInheritedEnterpriseRunnerGroupByName(client, ctx, org, enterpriseRunnerGroupName) + if err != nil { + return diag.FromErr(err) + } + + runnerGroupID := runnerGroup.GetID() + id, err := buildID(org, strconv.FormatInt(runnerGroupID, 10)) + if err != nil { + return diag.FromErr(err) + } + d.SetId(id) + + if err := d.Set("runner_group_id", int(runnerGroupID)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("inherited", runnerGroup.GetInherited()); err != nil { + return diag.FromErr(err) + } + + // Update runner group settings + updateReq := github.UpdateRunnerGroupRequest{ + Visibility: github.Ptr(visibility), + AllowsPublicRepositories: github.Ptr(allowsPublicRepositories), + RestrictedToWorkflows: github.Ptr(restrictedToWorkflows), + SelectedWorkflows: selectedWorkflows, + } + + _, _, err = client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq) + if err != nil { + return diag.Errorf("failed to update runner group: %s", err) + } + + // Set repository access if visibility is "selected" + if visibility == "selected" && hasSelectedRepositories { + selectedRepositoryIDs := []int64{} + for _, id := range selectedRepositories.(*schema.Set).List() { + selectedRepositoryIDs = append(selectedRepositoryIDs, int64(id.(int))) + } + + repoAccessReq := github.SetRepoAccessRunnerGroupRequest{ + SelectedRepositoryIDs: selectedRepositoryIDs, + } + + _, err = client.Actions.SetRepositoryAccessRunnerGroup(ctx, org, runnerGroupID, repoAccessReq) + if err != nil { + return diag.Errorf("failed to set repository access: %s", err) + } + } + + return nil +} + +func resourceGithubOrganizationInheritedRunnerGroupSettingsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + org := d.Get("organization").(string) + runnerGroupID := int64(d.Get("runner_group_id").(int)) + + // Get the runner group details + runnerGroup, _, err := client.Actions.GetOrganizationRunnerGroup(ctx, org, runnerGroupID) + if err != nil { + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusNotFound { + log.Printf("[INFO] Removing actions organization runner group %s from state because it no longer exists in GitHub", + d.Id()) + d.SetId("") + return nil + } + } + return diag.FromErr(err) + } + + if err := d.Set("inherited", runnerGroup.GetInherited()); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("visibility", runnerGroup.GetVisibility()); err != nil { + return diag.FromErr(err) + } + + // Get repository access list only if visibility is "selected" + if runnerGroup.GetVisibility() == "selected" { + selectedRepositoryIDs := []int64{} + opts := &github.ListOptions{ + PerPage: maxPerPage, + } + + for { + repos, resp, err := client.Actions.ListRepositoryAccessRunnerGroup(ctx, org, runnerGroupID, opts) + if err != nil { + return diag.Errorf("failed to list repository access: %s", err) + } + + for _, repo := range repos.Repositories { + selectedRepositoryIDs = append(selectedRepositoryIDs, repo.GetID()) + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + + if err := d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { + return diag.FromErr(err) + } + } else { + if err := d.Set("selected_repository_ids", []int64{}); err != nil { + return diag.FromErr(err) + } + } + + if err := d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("restricted_to_workflows", runnerGroup.GetRestrictedToWorkflows()); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("selected_workflows", runnerGroup.SelectedWorkflows); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubOrganizationInheritedRunnerGroupSettingsUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + org := d.Get("organization").(string) + runnerGroupID := int64(d.Get("runner_group_id").(int)) + visibility := d.Get("visibility").(string) + selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids") + + // Update runner group settings if any relevant fields changed + if d.HasChange("visibility") || d.HasChange("allows_public_repositories") || d.HasChange("restricted_to_workflows") || d.HasChange("selected_workflows") { + allowsPublicRepositories := d.Get("allows_public_repositories").(bool) + restrictedToWorkflows := d.Get("restricted_to_workflows").(bool) + + selectedWorkflows := []string{} + if workflows, ok := d.GetOk("selected_workflows"); ok { + for _, workflow := range workflows.([]any) { + selectedWorkflows = append(selectedWorkflows, workflow.(string)) + } + } + + updateReq := github.UpdateRunnerGroupRequest{ + Visibility: github.Ptr(visibility), + AllowsPublicRepositories: github.Ptr(allowsPublicRepositories), + RestrictedToWorkflows: github.Ptr(restrictedToWorkflows), + SelectedWorkflows: selectedWorkflows, + } + + _, _, err := client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq) + if err != nil { + return diag.Errorf("failed to update runner group: %s", err) + } + } + + // Update repository access if changed and visibility is "selected" + if d.HasChange("selected_repository_ids") && visibility == "selected" && hasSelectedRepositories { + selectedRepositoryIDs := []int64{} + + for _, id := range selectedRepositories.(*schema.Set).List() { + selectedRepositoryIDs = append(selectedRepositoryIDs, int64(id.(int))) + } + + repoAccessReq := github.SetRepoAccessRunnerGroupRequest{ + SelectedRepositoryIDs: selectedRepositoryIDs, + } + + _, err := client.Actions.SetRepositoryAccessRunnerGroup(ctx, org, runnerGroupID, repoAccessReq) + if err != nil { + return diag.Errorf("failed to set repository access: %s", err) + } + } + + return nil +} + +func resourceGithubOrganizationInheritedRunnerGroupSettingsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + + org := d.Get("organization").(string) + runnerGroupID := int64(d.Get("runner_group_id").(int)) + + log.Printf("[INFO] Removing repository access for runner group: %s", d.Id()) + + // Reset to "all" visibility and clear repository access + updateReq := github.UpdateRunnerGroupRequest{ + Visibility: github.Ptr("all"), + } + + _, _, err := client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq) + if err != nil { + // If the runner group doesn't exist, that's fine + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusNotFound { + return nil + } + } + return diag.Errorf("failed to reset runner group visibility: %s", err) + } + + return nil +} + +func resourceGithubOrganizationInheritedRunnerGroupSettingsImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + org, identifier, err := parseID2(d.Id()) + if err != nil { + return nil, fmt.Errorf("invalid import ID format, expected 'organization:enterprise_runner_group_name' or 'organization:organization_runner_group_id'") + } + + client := meta.(*Owner).v3client + + var runnerGroup *github.RunnerGroup + + // Try to parse as ID first + if id, parseErr := strconv.ParseInt(identifier, 10, 64); parseErr == nil { + // It's an ID - get the runner group and verify it's inherited + runnerGroup, _, err = client.Actions.GetOrganizationRunnerGroup(ctx, org, id) + if err != nil { + return nil, fmt.Errorf("failed to get runner group: %w", err) + } + } else { + // It's a name - find the inherited enterprise runner group + runnerGroup, err = findInheritedEnterpriseRunnerGroupByName(client, ctx, org, identifier) + if err != nil { + return nil, err + } + } + + // Verify the runner group is inherited from the enterprise + if !runnerGroup.GetInherited() { + return nil, fmt.Errorf("runner group '%s' is not inherited from the enterprise. This resource only manages inherited enterprise runner groups", runnerGroup.GetName()) + } + + id, err := buildID(org, strconv.FormatInt(runnerGroup.GetID(), 10)) + if err != nil { + return nil, err + } + d.SetId(id) + d.Set("organization", org) + d.Set("enterprise_runner_group_name", runnerGroup.GetName()) + d.Set("runner_group_id", int(runnerGroup.GetID())) + d.Set("inherited", runnerGroup.GetInherited()) + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_organization_inherited_runner_group_settings_test.go b/github/resource_github_organization_inherited_runner_group_settings_test.go new file mode 100644 index 0000000000..6cdd1b7afe --- /dev/null +++ b/github/resource_github_organization_inherited_runner_group_settings_test.go @@ -0,0 +1,329 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccGithubOrganizationInheritedRunnerGroupSettings(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + t.Run("manages repository access for enterprise runner group", func(t *testing.T) { + repoName := fmt.Sprintf("%srepo-%s", testResourcePrefix, randomID) + rgName := fmt.Sprintf("%srg-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test" { + name = "%s" + description = "Test repository for runner group access" + visibility = "private" + } + + resource "github_enterprise_actions_runner_group" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_organization_inherited_runner_group_settings" "test" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.test.name + selected_repository_ids = [github_repository.test.repo_id] + allows_public_repositories = true + } + `, testAccConf.enterpriseSlug, testAccConf.owner, repoName, rgName) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_organization_inherited_runner_group_settings.test", "runner_group_id", + ), + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "organization", + testAccConf.owner, + ), + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "allows_public_repositories", + "true", + ), + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "selected_repository_ids.#", + "1", + ), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) + + t.Run("updates repository access", func(t *testing.T) { + repoName1 := fmt.Sprintf("%srepo1-%s", testResourcePrefix, randomID) + repoName2 := fmt.Sprintf("%srepo2-%s", testResourcePrefix, randomID) + rgName := fmt.Sprintf("%srg-update-%s", testResourcePrefix, randomID) + + configCreate := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test1" { + name = "%s" + description = "Test repository 1 for runner group access" + visibility = "private" + } + + resource "github_repository" "test2" { + name = "%s" + description = "Test repository 2 for runner group access" + visibility = "private" + } + + resource "github_enterprise_actions_runner_group" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_organization_inherited_runner_group_settings" "test" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.test.name + selected_repository_ids = [github_repository.test1.repo_id] + } + `, testAccConf.enterpriseSlug, testAccConf.owner, repoName1, repoName2, rgName) + + configUpdate := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test1" { + name = "%s" + description = "Test repository 1 for runner group access" + visibility = "private" + } + + resource "github_repository" "test2" { + name = "%s" + description = "Test repository 2 for runner group access" + visibility = "private" + } + + resource "github_enterprise_actions_runner_group" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_organization_inherited_runner_group_settings" "test" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.test.name + selected_repository_ids = [github_repository.test1.repo_id, github_repository.test2.repo_id] + } + `, testAccConf.enterpriseSlug, testAccConf.owner, repoName1, repoName2, rgName) + + checkCreate := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "selected_repository_ids.#", + "1", + ), + ) + + checkUpdate := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "selected_repository_ids.#", + "2", + ), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configCreate, + Check: checkCreate, + }, + { + Config: configUpdate, + Check: checkUpdate, + }, + }, + }) + }) + + t.Run("manages workflow restrictions", func(t *testing.T) { + repoName := fmt.Sprintf("%srepo-wf-%s", testResourcePrefix, randomID) + rgName := fmt.Sprintf("%srg-wf-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test" { + name = "%s" + description = "Test repository for runner group access" + visibility = "private" + } + + resource "github_enterprise_actions_runner_group" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_organization_inherited_runner_group_settings" "test" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.test.name + selected_repository_ids = [github_repository.test.repo_id] + restricted_to_workflows = true + selected_workflows = ["${github_repository.test.full_name}/.github/workflows/test.yml@refs/heads/main"] + } + `, testAccConf.enterpriseSlug, testAccConf.owner, repoName, rgName) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_organization_inherited_runner_group_settings.test", "runner_group_id", + ), + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "restricted_to_workflows", + "true", + ), + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "selected_workflows.#", + "1", + ), + func(state *terraform.State) error { + githubRepository := state.RootModule().Resources["github_repository.test"].Primary + fullName := githubRepository.Attributes["full_name"] + + runnerGroup := state.RootModule().Resources["github_organization_inherited_runner_group_settings.test"].Primary + workflowActual := runnerGroup.Attributes["selected_workflows.0"] + + workflowExpected := fmt.Sprintf("%s/.github/workflows/test.yml@refs/heads/main", fullName) + + if workflowActual != workflowExpected { + return fmt.Errorf("expected selected_workflows.0 to be %s, got %s", workflowExpected, workflowActual) + } + return nil + }, + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) + + t.Run("imports runner group access by ID", func(t *testing.T) { + repoName := fmt.Sprintf("%srepo-import-%s", testResourcePrefix, randomID) + rgName := fmt.Sprintf("%srg-import-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test" { + name = "%s" + description = "Test repository for runner group access" + visibility = "private" + } + + resource "github_enterprise_actions_runner_group" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_organization_inherited_runner_group_settings" "test" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.test.name + selected_repository_ids = [github_repository.test.repo_id] + } + `, testAccConf.enterpriseSlug, testAccConf.owner, repoName, rgName) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_organization_inherited_runner_group_settings.test", "runner_group_id", + ), + resource.TestCheckResourceAttr( + "github_organization_inherited_runner_group_settings.test", "organization", + testAccConf.owner, + ), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessEnterprise(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + ResourceName: "github_organization_inherited_runner_group_settings.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: importOrganizationInheritedRunnerGroupSettingsByID("github_organization_inherited_runner_group_settings.test"), + }, + }, + }) + }) +} + +func importOrganizationInheritedRunnerGroupSettingsByID(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("resource not found: %s", resourceName) + } + return fmt.Sprintf("%s:%s", testAccConf.owner, rs.Primary.Attributes["runner_group_id"]), nil + } +} diff --git a/website/docs/r/organization_inherited_runner_group_settings.html.markdown b/website/docs/r/organization_inherited_runner_group_settings.html.markdown new file mode 100644 index 0000000000..1a38efbac9 --- /dev/null +++ b/website/docs/r/organization_inherited_runner_group_settings.html.markdown @@ -0,0 +1,161 @@ +--- +layout: "github" +page_title: "GitHub: github_organization_inherited_runner_group_settings" +description: |- + Manages organization-level settings for an enterprise Actions runner group inherited by the organization. +--- + +# github_organization_inherited_runner_group_settings + +This resource allows you to manage organization-level settings for **enterprise** Actions runner groups that are inherited by an organization. +When an enterprise runner group is shared with an organization (via `selected_organization_ids` in `github_enterprise_actions_runner_group`), +this resource allows you to configure which repositories within that organization can use the runner group, as well as visibility, workflow restrictions, and other settings. + +**Important:** This resource is specifically for managing inherited enterprise runner groups. It will not work with organization-level runner groups created directly in the organization. For organization-level runner groups, use the `github_actions_runner_group` resource instead. + +You must have admin access to the organization to use this resource. + +## Example Usage + +### Basic Usage + +```hcl +data "github_enterprise" "enterprise" { + slug = "my-enterprise" +} + +data "github_organization" "org" { + name = "my-organization" +} + +# Create a repository +resource "github_repository" "example" { + name = "example-repo" + description = "Example repository" + visibility = "private" +} + +# Create an enterprise runner group and share it with the organization +resource "github_enterprise_actions_runner_group" "example" { + name = "my-runner-group" + enterprise_slug = data.github_enterprise.enterprise.slug + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] +} + +# Configure repository access for the runner group in the organization +resource "github_organization_inherited_runner_group_settings" "example" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.example.name + selected_repository_ids = [github_repository.example.repo_id] + allows_public_repositories = true + restricted_to_workflows = true + selected_workflows = ["${github_repository.example.full_name}/.github/workflows/ci.yml@refs/heads/main"] +} +``` + +### Multiple Repositories + +```hcl +data "github_enterprise" "enterprise" { + slug = "my-enterprise" +} + +data "github_organization" "org" { + name = "my-organization" +} + +resource "github_repository" "repo1" { + name = "repo-1" + visibility = "private" +} + +resource "github_repository" "repo2" { + name = "repo-2" + visibility = "private" +} + +resource "github_enterprise_actions_runner_group" "example" { + name = "my-runner-group" + enterprise_slug = data.github_enterprise.enterprise.slug + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] +} + +resource "github_organization_inherited_runner_group_settings" "example" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.example.name + selected_repository_ids = [ + github_repository.repo1.repo_id, + github_repository.repo2.repo_id, + ] +} +``` + +### All Repositories (visibility = "all") + +```hcl +data "github_enterprise" "enterprise" { + slug = "my-enterprise" +} + +data "github_organization" "org" { + name = "my-organization" +} + +resource "github_enterprise_actions_runner_group" "example" { + name = "my-runner-group" + enterprise_slug = data.github_enterprise.enterprise.slug + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] +} + +# Make the runner group available to all repositories in the organization +resource "github_organization_inherited_runner_group_settings" "example" { + organization = data.github_organization.org.name + enterprise_runner_group_name = github_enterprise_actions_runner_group.example.name + visibility = "all" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `organization` - (Required) The GitHub organization name. +* `enterprise_runner_group_name` - (Required) The name of the enterprise runner group inherited by the organization. +* `visibility` - (Optional) The visibility of the runner group. Can be `all`, `selected`, or `private`. Defaults to `selected`. +* `selected_repository_ids` - (Optional) List of repository IDs that can access the runner group. Required when `visibility` is set to `selected`. +* `allows_public_repositories` - (Optional) Whether public repositories can be added to the runner group. Defaults to `false`. +* `restricted_to_workflows` - (Optional) If `true`, the runner group will be restricted to running only the workflows specified in the `selected_workflows` array. Defaults to `false`. +* `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`. The format is `{repo_full_name}/.github/workflows/{workflow_file}@{ref}` (e.g., `my-org/my-repo/.github/workflows/ci.yml@refs/heads/main`). + +## Attributes Reference + +The following additional attributes are exported: + +* `id` - The ID of the resource in the format `organization:runner_group_id`. +* `runner_group_id` - The ID of the inherited enterprise runner group in the organization. +* `inherited` - Whether this runner group is inherited from the enterprise (always `true` for this resource). + +## Import + +This resource can be imported using the organization name and either the organization runner group ID or name: + +``` +# Import using organization runner group ID +$ terraform import github_organization_inherited_runner_group_settings.example my-organization:123 + +# Import using runner group name +$ terraform import github_organization_inherited_runner_group_settings.example my-organization:my-runner-group +``` + +**Note:** The runner group ID used for import is the **organization-level** runner group ID, not the enterprise-level ID. These are different identifiers. + +## Notes + +* This resource **only** manages inherited enterprise runner groups. It will automatically verify that the runner group is inherited from the enterprise. +* The runner group must already exist at the enterprise level and be shared with the organization (via `selected_organization_ids` in `github_enterprise_actions_runner_group`). +* For organization-level runner groups (not inherited from enterprise), use the `github_actions_runner_group` resource instead. +* When this resource is destroyed, the runner group visibility is reset to `all`, making it available to all repositories in the organization. +* The runner group itself is not deleted when this resource is destroyed - only the repository access configuration is reset.