diff --git a/github/provider.go b/github/provider.go index 480bcba3fb..69fd521861 100644 --- a/github/provider.go +++ b/github/provider.go @@ -213,6 +213,7 @@ func Provider() *schema.Provider { "github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(), "github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(), "github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(), + "github_enterprise_actions_runner_group_org_settings": resourceGithubEnterpriseActionsRunnerGroupOrgSettings(), "github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(), "github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(), }, diff --git a/github/resource_github_enterprise_actions_runner_group_org_settings.go b/github/resource_github_enterprise_actions_runner_group_org_settings.go new file mode 100644 index 0000000000..3cc44048e9 --- /dev/null +++ b/github/resource_github_enterprise_actions_runner_group_org_settings.go @@ -0,0 +1,402 @@ +package github + +import ( + "context" + "fmt" + "log" + "net/http" + "strconv" + "strings" + + "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubEnterpriseActionsRunnerGroupOrgSettings() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubEnterpriseActionsRunnerGroupOrgSettingsCreate, + Read: resourceGithubEnterpriseActionsRunnerGroupOrgSettingsRead, + Update: resourceGithubEnterpriseActionsRunnerGroupOrgSettingsUpdate, + Delete: resourceGithubEnterpriseActionsRunnerGroupOrgSettingsDelete, + Importer: &schema.ResourceImporter{ + State: resourceGithubEnterpriseActionsRunnerGroupOrgSettingsImport, + }, + + 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, + ForceNew: 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: toDiagFunc(validation.StringInSlice([]string{"all", "selected", "private"}, false), "visibility"), + }, + "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 { + // Only match runner groups that are inherited from the enterprise + if group.GetName() == name && group.GetInherited() { + 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 resourceGithubEnterpriseActionsRunnerGroupOrgSettingsCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.Background() + + 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 err + } + + // Verify it's actually inherited + if !runnerGroup.GetInherited() { + return fmt.Errorf("runner group '%s' exists but is not inherited from the enterprise. This resource only manages inherited enterprise runner groups", enterpriseRunnerGroupName) + } + + runnerGroupID := runnerGroup.GetID() + d.SetId(fmt.Sprintf("%s:%d", org, runnerGroupID)) + + // Set the runner group ID and inherited flag + if err := d.Set("runner_group_id", int(runnerGroupID)); err != nil { + return err + } + if err := d.Set("inherited", runnerGroup.GetInherited()); err != nil { + return err + } + + // Update runner group settings + updateReq := github.UpdateRunnerGroupRequest{ + Visibility: github.String(visibility), + AllowsPublicRepositories: &allowsPublicRepositories, + RestrictedToWorkflows: &restrictedToWorkflows, + SelectedWorkflows: selectedWorkflows, + } + + _, _, err = client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq) + if err != nil { + return fmt.Errorf("failed to update runner group: %w", 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 fmt.Errorf("failed to set repository access: %w", err) + } + } + + return resourceGithubEnterpriseActionsRunnerGroupOrgSettingsRead(d, meta) +} + +func resourceGithubEnterpriseActionsRunnerGroupOrgSettingsRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + 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 err + } + + if runnerGroup == nil { + return nil + } + + // Verify it's still inherited from the enterprise + if !runnerGroup.GetInherited() { + log.Printf("[WARN] Runner group %s is no longer inherited from the enterprise", d.Id()) + } + + // Set inherited flag + if err := d.Set("inherited", runnerGroup.GetInherited()); err != nil { + return err + } + + // Set visibility + if err := d.Set("visibility", runnerGroup.GetVisibility()); err != nil { + return 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 fmt.Errorf("failed to list repository access: %w", 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 err + } + } else { + // Clear selected_repository_ids if visibility is not "selected" + if err := d.Set("selected_repository_ids", []int64{}); err != nil { + return err + } + } + + if err := d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()); 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 resourceGithubEnterpriseActionsRunnerGroupOrgSettingsUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + 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.String(visibility), + AllowsPublicRepositories: &allowsPublicRepositories, + RestrictedToWorkflows: &restrictedToWorkflows, + SelectedWorkflows: selectedWorkflows, + } + + _, _, err := client.Actions.UpdateOrganizationRunnerGroup(ctx, org, runnerGroupID, updateReq) + if err != nil { + return fmt.Errorf("failed to update runner group: %w", 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 fmt.Errorf("failed to set repository access: %w", err) + } + } + + return resourceGithubEnterpriseActionsRunnerGroupOrgSettingsRead(d, meta) +} + +func resourceGithubEnterpriseActionsRunnerGroupOrgSettingsDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + 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.String("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 fmt.Errorf("failed to reset runner group visibility: %w", err) + } + + return nil +} + +func resourceGithubEnterpriseActionsRunnerGroupOrgSettingsImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), ":") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid import ID format, expected 'organization:enterprise_runner_group_name' or 'organization:runner_group_id'") + } + + org := parts[0] + identifier := parts[1] + + client := meta.(*Owner).v3client + ctx := context.Background() + + var runnerGroup *github.RunnerGroup + var err error + + // 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()) + } + + d.SetId(fmt.Sprintf("%s:%d", org, runnerGroup.GetID())) + 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_enterprise_actions_runner_group_org_settings_test.go b/github/resource_github_enterprise_actions_runner_group_org_settings_test.go new file mode 100644 index 0000000000..b166bce074 --- /dev/null +++ b/github/resource_github_enterprise_actions_runner_group_org_settings_test.go @@ -0,0 +1,355 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccGithubEnterpriseActionsRunnerGroupOrgSettings(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + + if testEnterprise == "" { + t.Skip("Skipping because `ENTERPRISE_SLUG` is not set") + } + + if testOrganization == "" { + t.Skip("Skipping because `GITHUB_ORGANIZATION` is not set") + } + + t.Run("manages repository access for enterprise runner group", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + # Create a test repository + resource "github_repository" "test" { + name = "tf-acc-test-repo-%s" + description = "Test repository for runner group access" + visibility = "private" + } + + # Create an enterprise runner group + resource "github_enterprise_actions_runner_group" "test" { + enterprise_slug = data.github_enterprise.enterprise.slug + name = "tf-acc-test-rg-%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + # Configure repository access for the enterprise runner group + resource "github_enterprise_actions_runner_group_org_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 + } + `, testEnterprise, testOrganization, randomID, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_enterprise_actions_runner_group_org_settings.test", "runner_group_id", + ), + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_settings.test", "organization", + testOrganization, + ), + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_settings.test", "allows_public_repositories", + "true", + ), + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_settings.test", "selected_repository_ids.#", + "1", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an enterprise account", func(t *testing.T) { + testCase(t, enterprise) + }) + }) + + t.Run("updates repository access", func(t *testing.T) { + configCreate := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test1" { + name = "tf-acc-test-repo1-%s" + description = "Test repository 1 for runner group access" + visibility = "private" + } + + resource "github_repository" "test2" { + name = "tf-acc-test-repo2-%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 = "tf-acc-test-rg-%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_enterprise_actions_runner_group_org_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] + } + `, testEnterprise, testOrganization, randomID, randomID, randomID) + + configUpdate := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test1" { + name = "tf-acc-test-repo1-%s" + description = "Test repository 1 for runner group access" + visibility = "private" + } + + resource "github_repository" "test2" { + name = "tf-acc-test-repo2-%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 = "tf-acc-test-rg-%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_enterprise_actions_runner_group_org_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] + } + `, testEnterprise, testOrganization, randomID, randomID, randomID) + + checkCreate := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_settings.test", "selected_repository_ids.#", + "1", + ), + ) + + checkUpdate := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_settings.test", "selected_repository_ids.#", + "2", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: configCreate, + Check: checkCreate, + }, + { + Config: configUpdate, + Check: checkUpdate, + }, + }, + }) + } + + t.Run("with an enterprise account", func(t *testing.T) { + testCase(t, enterprise) + }) + }) + + t.Run("manages workflow restrictions", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test" { + name = "tf-acc-test-repo-%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 = "tf-acc-test-rg-%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_enterprise_actions_runner_group_org_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"] + } + `, testEnterprise, testOrganization, randomID, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_enterprise_actions_runner_group_org_settings.test", "runner_group_id", + ), + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_settings.test", "restricted_to_workflows", + "true", + ), + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_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_enterprise_actions_runner_group_org_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 + }, + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an enterprise account", func(t *testing.T) { + testCase(t, enterprise) + }) + }) + + t.Run("imports runner group access by ID", func(t *testing.T) { + config := fmt.Sprintf(` + data "github_enterprise" "enterprise" { + slug = "%s" + } + + data "github_organization" "org" { + name = "%s" + } + + resource "github_repository" "test" { + name = "tf-acc-test-repo-%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 = "tf-acc-test-rg-%s" + visibility = "selected" + selected_organization_ids = [data.github_organization.org.id] + } + + resource "github_enterprise_actions_runner_group_org_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] + } + `, testEnterprise, testOrganization, randomID, randomID) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "github_enterprise_actions_runner_group_org_settings.test", "runner_group_id", + ), + resource.TestCheckResourceAttr( + "github_enterprise_actions_runner_group_org_settings.test", "organization", + testOrganization, + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + ResourceName: "github_enterprise_actions_runner_group_org_settings.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: importEnterpriseActionsRunnerGroupOrgSettingsByID("github_enterprise_actions_runner_group_org_settings.test"), + }, + }, + }) + } + + t.Run("with an enterprise account", func(t *testing.T) { + testCase(t, enterprise) + }) + }) +} + +func importEnterpriseActionsRunnerGroupOrgSettingsByID(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", testOrganization, rs.Primary.Attributes["runner_group_id"]), nil + } +} diff --git a/website/docs/r/enterprise_actions_runner_group_org_settings.html.markdown b/website/docs/r/enterprise_actions_runner_group_org_settings.html.markdown new file mode 100644 index 0000000000..3687a78e38 --- /dev/null +++ b/website/docs/r/enterprise_actions_runner_group_org_settings.html.markdown @@ -0,0 +1,159 @@ +--- +layout: "github" +page_title: "GitHub: github_enterprise_actions_runner_group_org_settings" +description: |- + Manages repository access for an enterprise Actions Runner Group at the organization level. +--- + +# github_enterprise_actions_runner_group_org_settings + +This resource allows you to manage repository access 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. + +**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_enterprise_actions_runner_group_org_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_enterprise_actions_runner_group_org_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_enterprise_actions_runner_group_org_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 runner group ID or name: + +``` +# Import using runner group ID +$ terraform import github_enterprise_actions_runner_group_org_settings.example my-organization:123 + +# Import using runner group name +$ terraform import github_enterprise_actions_runner_group_org_settings.example my-organization:my-runner-group +``` + +## 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.