From f89cd45a29517fcb653fd916089c70bd48b08334 Mon Sep 17 00:00:00 2001 From: AJ Bowen Date: Sun, 13 Jun 2021 18:41:18 +0200 Subject: [PATCH] WIP --- github/provider.go | 1 + .../resource_github_actions_runner_group.go | 225 ++++++++++++++++ ...source_github_actions_runner_group_test.go | 249 ++++++++++++++++++ 3 files changed, 475 insertions(+) create mode 100644 github/resource_github_actions_runner_group.go create mode 100644 github/resource_github_actions_runner_group_test.go diff --git a/github/provider.go b/github/provider.go index f75293706e..ce87cb21f8 100644 --- a/github/provider.go +++ b/github/provider.go @@ -75,6 +75,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "github_actions_organization_secret": resourceGithubActionsOrganizationSecret(), + "github_actions_runner_group": resourceGithubActionsRunnerGroup(), "github_actions_secret": resourceGithubActionsSecret(), "github_app_installation_repository": resourceGithubAppInstallationRepository(), "github_branch": resourceGithubBranch(), diff --git a/github/resource_github_actions_runner_group.go b/github/resource_github_actions_runner_group.go new file mode 100644 index 0000000000..2b43007607 --- /dev/null +++ b/github/resource_github_actions_runner_group.go @@ -0,0 +1,225 @@ +package github + +import ( + "context" + "fmt" + "log" + "net/http" + "strconv" + + "github.com/google/go-github/v35/github" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" +) + +func resourceGithubActionsRunnerGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubActionsRunnerGroupCreate, + Read: resourceGithubActionsRunnerGroupRead, + Update: resourceGithubActionsRunnerGroupUpdate, + Delete: resourceGithubActionsRunnerGroupDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "allows_public_repositories": { + Type: schema.TypeBool, + Computed: true, + }, + "default": { + Type: schema.TypeBool, + Computed: true, + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, + "inherited": { + Type: schema.TypeBool, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "runners_url": { + Type: schema.TypeString, + Computed: true, + }, + "selected_repository_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Set: schema.HashInt, + Optional: true, + }, + "selected_repositories_url": { + Type: schema.TypeString, + Computed: true, + }, + "visibility": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"all", "selected", "private"}, false), + }, + }, + } +} + +func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + name := d.Get("name").(string) + visibility := d.Get("visibility").(string) + selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids") + + if visibility != "selected" && hasSelectedRepositories { + return fmt.Errorf("Cannot use selected_repository_ids without visibility being set to selected") + } + + selectedRepositoryIDs := []int64{} + + if hasSelectedRepositories { + ids := selectedRepositories.(*schema.Set).List() + + for _, id := range ids { + selectedRepositoryIDs = append(selectedRepositoryIDs, int64(id.(int))) + } + } + + // TODO: also get runners + // runners := d.Get("runners").([]int64) + ctx := context.Background() + + log.Printf("[DEBUG] Creating organization runner group: %s (%s)", name, orgName) + runnerGroup, resp, err := client.Actions.CreateOrganizationRunnerGroup(ctx, + orgName, + github.CreateRunnerGroupRequest{ + Name: &name, + Visibility: &visibility, + SelectedRepositoryIDs: selectedRepositoryIDs, + // TODO + // Runners: runners, + }, + ) + if err != nil { + return err + } + d.SetId(strconv.FormatInt(runnerGroup.GetID(), 10)) + d.Set("etag", resp.Header.Get("ETag")) + d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()) + d.Set("default", runnerGroup.GetDefault()) + d.Set("id", runnerGroup.GetID()) + d.Set("inherited", runnerGroup.GetInherited()) + d.Set("name", runnerGroup.GetName()) + d.Set("runners_url", runnerGroup.GetRunnersURL()) + d.Set("selected_repositories_url", runnerGroup.GetSelectedRepositoriesURL()) + d.Set("visibility", runnerGroup.GetVisibility()) + + return resourceGithubActionsRunnerGroupRead(d, meta) +} + +func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + + runnerGroupID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return err + } + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + if !d.IsNewResource() { + ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) + } + + log.Printf("[DEBUG] Reading organization runner group: %s (%s)", d.Id(), orgName) + runnerGroup, resp, err := client.Actions.GetOrganizationRunnerGroup(ctx, orgName, runnerGroupID) + if err != nil { + if ghErr, ok := err.(*github.ErrorResponse); ok { + if ghErr.Response.StatusCode == http.StatusNotModified { + return nil + } + if ghErr.Response.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Removing organization runner group %s/%s from state because it no longer exists in GitHub", + orgName, d.Id()) + d.SetId("") + return nil + } + } + return err + } + + d.Set("etag", resp.Header.Get("ETag")) + d.Set("allows_public_repositories", runnerGroup.GetAllowsPublicRepositories()) + d.Set("default", runnerGroup.GetDefault()) + d.Set("id", runnerGroup.GetID()) + d.Set("inherited", runnerGroup.GetInherited()) + d.Set("name", runnerGroup.GetName()) + d.Set("runners_url", runnerGroup.GetRunnersURL()) + d.Set("selected_repositories_url", runnerGroup.GetSelectedRepositoriesURL()) + d.Set("visibility", runnerGroup.GetVisibility()) + + return nil +} + +func resourceGithubActionsRunnerGroupUpdate(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + + name := d.Get("name").(string) + visibility := d.Get("visibility").(string) + + options := github.UpdateRunnerGroupRequest{ + Name: &name, + Visibility: &visibility, + } + + runnerGroupID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return err + } + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + log.Printf("[DEBUG] Updating organization runner group: %s (%s)", d.Id(), orgName) + if _, _, err := client.Actions.UpdateOrganizationRunnerGroup(ctx, orgName, runnerGroupID, options); err != nil { + return err + } + + return resourceGithubActionsRunnerGroupRead(d, meta) +} + +func resourceGithubActionsRunnerGroupDelete(d *schema.ResourceData, meta interface{}) error { + err := checkOrganization(meta) + if err != nil { + return err + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + runnerGroupID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return err + } + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + log.Printf("[DEBUG] Deleting organization runner group: %s (%s)", d.Id(), orgName) + _, err = client.Actions.DeleteOrganizationRunnerGroup(ctx, orgName, runnerGroupID) + return err +} diff --git a/github/resource_github_actions_runner_group_test.go b/github/resource_github_actions_runner_group_test.go new file mode 100644 index 0000000000..77193527e5 --- /dev/null +++ b/github/resource_github_actions_runner_group_test.go @@ -0,0 +1,249 @@ +package github + +import ( + "context" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/google/go-github/v35/github" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccGithubActionsRunnerGroup_all(t *testing.T) { + // ??? + // resource_github_actions_runner_group_test.go:19: Skipping because GITHUB_OWNER is a user, not an organization. + // if err := testAccCheckOrganization(); err != nil { + // t.Skipf("Skipping because %s.", err.Error()) + // } + + var runnerGroup github.RunnerGroup + + var testAccGithubActionsRunnerGroupConfigAll = ` +resource "github_actions_runner_group" "test_all" { + name = "test-runner-group-all" + visibility = "all" +} +` + rn := "github_actions_runner_group.test_all" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGithubActionsRunnerGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubActionsRunnerGroupConfigAll, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubActionsRunnerGroupExists(rn, &runnerGroup), + testAccCheckGithubActionsRunnerGroupAttributes(&runnerGroup, &testAccGithubActionsRunnerGroupExpectedAttributes{ + Name: "test-runner-group-all", + Visibility: "all", + Default: false, + AllowsPublicRepositories: false, + RunnersURL: fmt.Sprintf(`https://api.github.com/orgs/octo-org/actions/runner_groups/%d/runners`, runnerGroup.ID), + SelectedRepositoriesURL: fmt.Sprintf(`https://api.github.com/orgs/octo-org/actions/runner_groups/%d/repositories`, runnerGroup.ID), + }), + ), + }, + { + ResourceName: rn, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccGithubActionsRunnerGroup_private(t *testing.T) { + // ??? + // resource_github_actions_runner_group_test.go:19: Skipping because GITHUB_OWNER is a user, not an organization. + // if err := testAccCheckOrganization(); err != nil { + // t.Skipf("Skipping because %s.", err.Error()) + // } + + var runnerGroup github.RunnerGroup + + rn := "github_actions_runner_group.test_private" + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + var testAccGithubActionsRunnerGroupConfigPrivate = fmt.Sprintf(` +resource "github_repository" "test" { + name = "tf-acc-test-%s" + visibility = "private" +} +resource "github_actions_runner_group" "test_private" { + name = "test-runner-group-private" + visibility = "private" +} +`, randomID) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGithubActionsRunnerGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubActionsRunnerGroupConfigPrivate, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubActionsRunnerGroupExists(rn, &runnerGroup), + testAccCheckGithubActionsRunnerGroupAttributes(&runnerGroup, &testAccGithubActionsRunnerGroupExpectedAttributes{ + Name: "test-runner-group-private", + Visibility: "private", + Default: false, + AllowsPublicRepositories: false, + RunnersURL: fmt.Sprintf(`https://api.github.com/orgs/octo-org/actions/runner_groups/%d/runners`, runnerGroup.ID), + SelectedRepositoriesURL: fmt.Sprintf(`https://api.github.com/orgs/octo-org/actions/runner_groups/%d/repositories`, runnerGroup.ID), + }), + ), + }, + { + ResourceName: rn, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccGithubActionsRunnerGroup_selected(t *testing.T) { + // ??? + // resource_github_actions_runner_group_test.go:19: Skipping because GITHUB_OWNER is a user, not an organization. + // if err := testAccCheckOrganization(); err != nil { + // t.Skipf("Skipping because %s.", err.Error()) + // } + + var runnerGroup github.RunnerGroup + + rn := "github_actions_runner_group.test_selected" + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + var testAccGithubActionsRunnerGroupConfigSelected = fmt.Sprintf(` +resource "github_repository" "test" { + name = "tf-acc-test-%s" + auto_init = true +} + +resource "github_actions_runner_group" "test_selected" { + name = "test-runner-group-selected" + visibility = "selected" + selected_repository_ids = [github_repository.test.repo_id] +} +`, randomID) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccGithubActionsRunnerGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccGithubActionsRunnerGroupConfigSelected, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubActionsRunnerGroupExists(rn, &runnerGroup), + testAccCheckGithubActionsRunnerGroupAttributes(&runnerGroup, &testAccGithubActionsRunnerGroupExpectedAttributes{ + Name: "test-runner-group-selected", + Visibility: "selected", + Default: false, + AllowsPublicRepositories: false, + RunnersURL: fmt.Sprintf(`https://api.github.com/orgs/octo-org/actions/runner_groups/%d/runners`, runnerGroup.ID), + SelectedRepositoriesURL: fmt.Sprintf(`https://api.github.com/orgs/octo-org/actions/runner_groups/%d/repositories`, runnerGroup.ID), + }), + ), + }, + { + ResourceName: rn, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccGithubActionsRunnerGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Owner).v3client + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_actions_runner_group" { + continue + } + + runnerGroupID, err := strconv.ParseInt(rs.Primary.ID, 10, 64) + if err != nil { + return err + } + + orgName := testAccProvider.Meta().(*Owner).name + runnerGroup, res, err := conn.Actions.GetOrganizationRunnerGroup(context.TODO(), orgName, runnerGroupID) + if err == nil { + if runnerGroup != nil && + runnerGroup.GetID() == runnerGroupID { + return fmt.Errorf("Organization runner group still exists") + } + } + if res.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +func testAccCheckGithubActionsRunnerGroupExists(n string, runnerGroup *github.RunnerGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + runnerGroupID, err := strconv.ParseInt(rs.Primary.ID, 10, 64) + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*Owner).v3client + orgName := testAccProvider.Meta().(*Owner).name + gotRunnerGroup, _, err := conn.Actions.GetOrganizationRunnerGroup(context.TODO(), orgName, runnerGroupID) + if err != nil { + return err + } + *runnerGroup = *gotRunnerGroup + return nil + } +} + +type testAccGithubActionsRunnerGroupExpectedAttributes struct { + AllowsPublicRepositories bool + Default bool + ID int64 + Inherited bool + Name string + Runners []int64 + RunnersURL string + SelectedRepositoriesURL string + SelectedRepositoryIDs []int64 + Visibility string +} + +func testAccCheckGithubActionsRunnerGroupAttributes(runnerGroup *github.RunnerGroup, want *testAccGithubActionsRunnerGroupExpectedAttributes) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if name := runnerGroup.GetName(); name != want.Name { + return fmt.Errorf("got runnerGroup name %q; want %q", name, want.Name) + } + if visibility := runnerGroup.GetVisibility(); visibility != want.Visibility { + return fmt.Errorf("got runnerGroup visibility %q; want %q", visibility, want.Visibility) + } + if inherited := runnerGroup.GetInherited(); inherited != want.Inherited { + return fmt.Errorf("got runnerGroup inherited %t; want %t", inherited, want.Inherited) + } + if URL := runnerGroup.GetRunnersURL(); !strings.HasPrefix(URL, "https://") { + return fmt.Errorf("got runners URL %q; want to start with 'https://'", URL) + } + if isDefault := runnerGroup.GetDefault(); isDefault != want.Default { + return fmt.Errorf("got runnerGroup default %t; want %t", isDefault, want.Default) + } + + return nil + } +}