-
Notifications
You must be signed in to change notification settings - Fork 774
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support for issues using github_issue resource
Introduce support for managing github issues using the terraform provider. Closes: #958 Signed-off-by: Edward Wilde <[email protected]>
- Loading branch information
1 parent
f9508c5
commit 7877ec4
Showing
8 changed files
with
630 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package github | ||
|
||
import ( | ||
"context" | ||
"github.com/google/go-github/v42/github" | ||
"github.com/hashicorp/terraform-plugin-sdk/helper/schema" | ||
"log" | ||
"net/http" | ||
"strconv" | ||
) | ||
|
||
func resourceGithubIssue() *schema.Resource { | ||
return &schema.Resource{ | ||
Create: resourceGithubIssueCreateOrUpdate, | ||
Read: resourceGithubIssueRead, | ||
Update: resourceGithubIssueCreateOrUpdate, | ||
Delete: resourceGithubIssueDelete, | ||
Importer: &schema.ResourceImporter{ | ||
State: schema.ImportStatePassthrough, | ||
}, | ||
Schema: map[string]*schema.Schema{ | ||
"repository": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
}, | ||
"number": { | ||
Type: schema.TypeInt, | ||
Required: false, | ||
Computed: true, | ||
}, | ||
"title": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
}, | ||
"body": { | ||
Type: schema.TypeString, | ||
Optional: true, | ||
}, | ||
"labels": { | ||
Type: schema.TypeSet, | ||
Elem: &schema.Schema{Type: schema.TypeString}, | ||
Set: schema.HashString, | ||
Optional: true, | ||
Description: "List of names of labels on the issue", | ||
}, | ||
"assignees": { | ||
Type: schema.TypeSet, | ||
Elem: &schema.Schema{Type: schema.TypeString}, | ||
Set: schema.HashString, | ||
Optional: true, | ||
Description: "List of Logins for Users to assign to this issue", | ||
}, | ||
"milestone_number": { | ||
Type: schema.TypeInt, | ||
Optional: true, | ||
}, | ||
"issue_id": { | ||
Type: schema.TypeInt, | ||
Computed: true, | ||
}, | ||
"etag": { | ||
Type: schema.TypeString, | ||
Computed: true, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func resourceGithubIssueCreateOrUpdate(d *schema.ResourceData, meta interface{}) error { | ||
ctx := context.Background() | ||
client := meta.(*Owner).v3client | ||
orgName := meta.(*Owner).name | ||
repoName := d.Get("repository").(string) | ||
title := d.Get("title").(string) | ||
milestone := d.Get("milestone_number").(int) | ||
|
||
req := &github.IssueRequest{ | ||
Title: github.String(title), | ||
} | ||
|
||
if v, ok := d.GetOk("body"); ok { | ||
req.Body = github.String(v.(string)) | ||
} | ||
|
||
labels := expandStringList(d.Get("labels").(*schema.Set).List()) | ||
req.Labels = &labels | ||
|
||
assignees := expandStringList(d.Get("assignees").(*schema.Set).List()) | ||
req.Assignees = &assignees | ||
|
||
if milestone > 0 { | ||
req.Milestone = intPtr(milestone) | ||
} | ||
|
||
var issue *github.Issue | ||
var resp *github.Response | ||
var err error | ||
if d.IsNewResource() { | ||
log.Printf("[DEBUG] Creating issue: %s (%s/%s)", | ||
title, orgName, repoName) | ||
issue, resp, err = client.Issues.Create(ctx, orgName, repoName, req) | ||
if resp != nil { | ||
log.Printf("[DEBUG] Response from creating issue: %#v", *resp) | ||
} | ||
} else { | ||
number := d.Get("number").(int) | ||
log.Printf("[DEBUG] Updating issue: %d:%s (%s/%s)", | ||
number, title, orgName, repoName) | ||
issue, resp, err = client.Issues.Edit(ctx, orgName, repoName, number , req) | ||
if resp != nil { | ||
log.Printf("[DEBUG] Response from updating issue: %#v", *resp) | ||
} | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
|
||
d.SetId(buildTwoPartID(repoName, strconv.Itoa(issue.GetNumber()))) | ||
d.Set("issue_id", issue.GetID()) | ||
return resourceGithubIssueRead(d, meta) | ||
} | ||
|
||
func resourceGithubIssueRead(d *schema.ResourceData, meta interface{}) error { | ||
client := meta.(*Owner).v3client | ||
repoName, idNumber, err := parseTwoPartID(d.Id(), "repository", "issue_number") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
number, err := strconv.Atoi(idNumber) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
orgName := meta.(*Owner).name | ||
ctx := context.WithValue(context.Background(), ctxId, d.Id()) | ||
if !d.IsNewResource() { | ||
ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) | ||
} | ||
|
||
log.Printf("[DEBUG] Reading issue: %d (%s/%s)", number, orgName, repoName) | ||
issue, resp, err := client.Issues.Get(ctx, | ||
orgName, repoName, number) | ||
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 issue %d (%s/%s) from state because it no longer exists in GitHub", | ||
number, orgName, repoName) | ||
d.SetId("") | ||
return nil | ||
} | ||
} | ||
return err | ||
} | ||
|
||
d.Set("etag", resp.Header.Get("ETag")) | ||
d.Set("repository", repoName) | ||
d.Set("number", number) | ||
d.Set("title", issue.GetTitle()) | ||
d.Set("body", issue.GetBody()) | ||
d.Set("milestone_number", issue.GetMilestone().GetNumber()) | ||
|
||
var labels []string | ||
for _, v := range issue.Labels { | ||
labels = append(labels, v.GetName()) | ||
} | ||
d.Set("labels", flattenStringList(labels)) | ||
|
||
var assignees []string | ||
for _, v := range issue.Assignees { | ||
assignees = append(assignees, v.GetLogin()) | ||
} | ||
d.Set("assignees", flattenStringList(assignees)) | ||
|
||
d.Set("issue_id", issue.GetID()) | ||
return nil | ||
} | ||
|
||
func resourceGithubIssueDelete(d *schema.ResourceData, meta interface{}) error { | ||
client := meta.(*Owner).v3client | ||
|
||
orgName := meta.(*Owner).name | ||
repoName := d.Get("repository").(string) | ||
number := d.Get("number").(int) | ||
ctx := context.WithValue(context.Background(), ctxId, d.Id()) | ||
|
||
log.Printf("[DEBUG] Deleting issue by closing: %d (%s/%s)", number, orgName, repoName) | ||
|
||
request := &github.IssueRequest{State: github.String("closed")} | ||
|
||
_, _, err := client.Issues.Edit(ctx, orgName, repoName, number, request) | ||
|
||
return err | ||
} | ||
|
||
func intPtr(i int) *int { | ||
return &i | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
package github | ||
|
||
import ( | ||
"fmt" | ||
"github.com/hashicorp/terraform-plugin-sdk/terraform" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-sdk/helper/acctest" | ||
"github.com/hashicorp/terraform-plugin-sdk/helper/resource" | ||
) | ||
|
||
func TestAccGithubIssue(t *testing.T) { | ||
|
||
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) | ||
|
||
t.Run("creates an issue without error", func(t *testing.T) { | ||
|
||
title := "issue_title" | ||
body := "issue_body" | ||
labels := "\"bug\", \"enhancement\"" | ||
updatedTitle := "updated_issue_title" | ||
updatedBody := "update_issue_body" | ||
updatedLabels := "\"documentation\"" | ||
|
||
issueHCL := ` | ||
resource "github_repository" "test" { | ||
name = "tf-acc-test-%s" | ||
auto_init = true | ||
has_issues = true | ||
} | ||
resource "github_repository_milestone" "test" { | ||
owner = split("/", "${github_repository.test.full_name}")[0] | ||
repository = github_repository.test.name | ||
title = "v1.0.0" | ||
description = "General Availability" | ||
due_date = "2022-11-22" | ||
state = "open" | ||
} | ||
resource "github_issue" "test" { | ||
repository = github_repository.test.name | ||
title = "%s" | ||
body = "%s" | ||
labels = [%s] | ||
assignees = ["%s"] | ||
milestone_number = github_repository_milestone.test.number | ||
} | ||
` | ||
config := fmt.Sprintf(issueHCL, randomID, title, body, labels, testOwnerFunc()) | ||
|
||
checks := map[string]resource.TestCheckFunc{ | ||
"before": resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr( | ||
"github_issue.test", "title", | ||
title, | ||
), | ||
resource.TestCheckResourceAttr( | ||
"github_issue.test", "body", | ||
body, | ||
), | ||
resource.TestCheckResourceAttr( | ||
"github_issue.test", "labels.#", | ||
"2", | ||
), | ||
func(state *terraform.State) error { | ||
issue := state.RootModule().Resources["github_issue.test"].Primary | ||
issueMilestone := issue.Attributes["milestone_number"] | ||
|
||
milestone := state.RootModule().Resources["github_repository_milestone.test"].Primary | ||
milestoneNumber := milestone.Attributes["number"] | ||
if issueMilestone != milestoneNumber { | ||
return fmt.Errorf("issue milestone number %s not the same as repository milestone number %s", | ||
issueMilestone, milestoneNumber) | ||
} | ||
return nil | ||
}, | ||
), | ||
"after": resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttr( | ||
"github_issue.test", "title", | ||
updatedTitle, | ||
), | ||
resource.TestCheckResourceAttr( | ||
"github_issue.test", "body", | ||
updatedBody, | ||
), resource.TestCheckResourceAttr( | ||
"github_issue.test", "labels.#", | ||
"1", | ||
), resource.TestCheckResourceAttr( | ||
"github_issue.test", "assignees.#", | ||
"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: checks["before"], | ||
}, | ||
{ | ||
Config: fmt.Sprintf(issueHCL, randomID, updatedTitle, updatedBody, updatedLabels, testOwnerFunc()), | ||
Check: checks["after"], | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
t.Run("with an anonymous account", func(t *testing.T) { | ||
t.Skip("anonymous account not supported for this operation") | ||
}) | ||
|
||
t.Run("with an individual account", func(t *testing.T) { | ||
testCase(t, individual) | ||
}) | ||
|
||
t.Run("with an organization account", func(t *testing.T) { | ||
testCase(t, organization) | ||
}) | ||
}) | ||
|
||
t.Run("imports a issue without error", func(t *testing.T) { | ||
config := fmt.Sprintf(` | ||
resource "github_repository" "test" { | ||
name = "tf-acc-test-%s" | ||
has_issues = true | ||
} | ||
resource "github_issue" "test" { | ||
repository = github_repository.test.name | ||
title = github_repository.test.name | ||
} | ||
`, randomID) | ||
|
||
check := resource.ComposeTestCheckFunc( | ||
resource.TestCheckResourceAttrSet("github_issue.test", "title"), | ||
resource.TestCheckResourceAttr("github_issue.test", "title", fmt.Sprintf(`tf-acc-test-%s`, randomID)), | ||
) | ||
|
||
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_issue.test", | ||
ImportState: true, | ||
ImportStateVerify: true, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
t.Run("with an anonymous account", func(t *testing.T) { | ||
t.Skip("anonymous account not supported for this operation") | ||
}) | ||
|
||
t.Run("with an individual account", func(t *testing.T) { | ||
testCase(t, individual) | ||
}) | ||
|
||
t.Run("with an organization account", func(t *testing.T) { | ||
testCase(t, organization) | ||
}) | ||
}) | ||
|
||
} |
Oops, something went wrong.