Merge pull request #12 from Kamahl19/default_reviewers_condition

Implement resource bitbucketserver_default_reviewers_condition
This commit is contained in:
Gavin Bunney
2020-07-17 21:54:55 -07:00
committed by GitHub
6 changed files with 575 additions and 0 deletions

View File

@@ -48,6 +48,7 @@ func Provider() terraform.ResourceProvider {
},
ResourcesMap: map[string]*schema.Resource{
"bitbucketserver_banner": resourceBanner(),
"bitbucketserver_default_reviewers_condition": resourceDefaultReviewersCondition(),
"bitbucketserver_global_permissions_group": resourceGlobalPermissionsGroup(),
"bitbucketserver_global_permissions_user": resourceGlobalPermissionsUser(),
"bitbucketserver_group": resourceGroup(),

View File

@@ -0,0 +1,369 @@
package bitbucket
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/schema"
)
type Reviewer struct {
ID int `json:"id,omitempty"`
}
type MatcherType struct {
ID string `json:"id,omitempty"`
}
type Matcher struct {
ID string `json:"id,omitempty"`
Type MatcherType `json:"type,omitempty"`
}
type RefMatcher struct {
ID string `json:"id,omitempty"`
Type struct {
ID string `json:"id,omitempty"`
} `json:"type,omitempty"`
}
type DefaultReviewersConditionPayload struct {
SourceMatcher Matcher `json:"sourceMatcher,omitempty"`
TargetMatcher Matcher `json:"targetMatcher,omitempty"`
Reviewers []Reviewer `json:"reviewers,omitempty"`
RequiredApprovals int `json:"requiredApprovals,omitempty"`
}
type DefaultReviewersConditionResp struct {
ID int `json:"id,omitempty"`
RequiredApprovals int `json:"requiredApprovals,omitempty"`
Reviewers []Reviewer `json:"reviewers,omitempty"`
SourceRefMatcher RefMatcher `json:"sourceRefMatcher,omitempty"`
TargetRefMatcher RefMatcher `json:"targetRefMatcher,omitempty"`
}
var matcherDesc = `id can be either "any" to match all branches, "refs/heads/master" to match certain branch, "pattern" to match multiple branches or "development" to match branching model. type_id must be one of: "ANY_REF", "BRANCH", "PATTERN", "MODEL_BRANCH".`
var validMatcherTypeIDs = []string{
"ANY_REF", "BRANCH", "PATTERN", "MODEL_BRANCH",
}
func resourceDefaultReviewersCondition() *schema.Resource {
return &schema.Resource{
Create: resourceDefaultReviewersConditionCreate,
Read: resourceDefaultReviewersConditionRead,
Delete: resourceDefaultReviewersConditionDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"project_key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"repository_slug": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"source_matcher": {
Type: schema.TypeMap,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Required: true,
ForceNew: true,
Description: matcherDesc,
},
"target_matcher": {
Type: schema.TypeMap,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Required: true,
ForceNew: true,
Description: matcherDesc,
},
"reviewers": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeInt},
Required: true,
Set: schema.HashInt,
ForceNew: true,
MinItems: 1,
Description: "IDs of users to become default reviewers when you create a pull request.",
},
"required_approvals": {
Type: schema.TypeInt,
Required: true,
ForceNew: true,
Description: "The number of default reviewers that must approve a pull request.",
},
},
}
}
func refMatcherToMatcher(refMatcher RefMatcher) Matcher {
convertID := func(id string) string {
if id == "ANY_REF_MATCHER_ID" {
return "any"
}
return id
}
return Matcher{
ID: convertID(refMatcher.ID),
Type: MatcherType{
ID: refMatcher.Type.ID,
},
}
}
func expandReviewers(set *schema.Set) []Reviewer {
list := set.List()
rs := make([]Reviewer, 0, len(list))
for _, v := range list {
rs = append(rs, Reviewer{ID: v.(int)})
}
return rs
}
func collapseReviewers(reviewers []Reviewer) *schema.Set {
reviewerIDs := make([]interface{}, 0)
for _, r := range reviewers {
reviewerIDs = append(reviewerIDs, r.ID)
}
return schema.NewSet(schema.HashInt, reviewerIDs)
}
func expandMatcher(matcherMap map[string]interface{}) Matcher {
return Matcher{
ID: matcherMap["id"].(string),
Type: MatcherType{
ID: matcherMap["type_id"].(string),
},
}
}
func collapseMatcher(matcher Matcher) map[string]interface{} {
return map[string]interface{}{
"id": matcher.ID,
"type_id": matcher.Type.ID,
}
}
func parseResourceID(id string) (string, string, string, error) {
parts := strings.Split(id, ":")
errMsg := fmt.Errorf("invalid format of ID (%s), expected condition_id:project_key or condition_id:project_key:repository_slug", id)
if len(parts) != 2 && len(parts) != 3 {
return "", "", "", errMsg
}
if len(parts) == 2 {
if parts[0] == "" || parts[1] == "" {
return "", "", "", errMsg
}
return parts[0], parts[1], "", nil
}
if parts[0] == "" || parts[1] == "" || parts[2] == "" {
return "", "", "", errMsg
}
return parts[0], parts[1], parts[2], nil
}
func createResourceID(conditionID int, projectKey string, repositorySlug string) string {
if repositorySlug == "" {
return fmt.Sprintf("%v:%s", conditionID, projectKey)
}
return fmt.Sprintf("%v:%s:%s", conditionID, projectKey, repositorySlug)
}
func getCreateConditionURI(projectKey string, repositorySlug string) string {
if repositorySlug == "" {
return fmt.Sprintf("/rest/default-reviewers/1.0/projects/%s/condition",
url.PathEscape(projectKey),
)
}
return fmt.Sprintf("/rest/default-reviewers/1.0/projects/%s/repos/%s/condition",
url.PathEscape(projectKey),
url.PathEscape(repositorySlug),
)
}
func getReadConditionURI(projectKey string, repositorySlug string) string {
if repositorySlug == "" {
return fmt.Sprintf("/rest/default-reviewers/1.0/projects/%s/conditions",
url.PathEscape(projectKey),
)
}
return fmt.Sprintf("/rest/default-reviewers/1.0/projects/%s/repos/%s/conditions",
url.PathEscape(projectKey),
url.PathEscape(repositorySlug),
)
}
func getDeleteConditionURI(conditionID string, projectKey string, repositorySlug string) string {
if repositorySlug == "" {
return fmt.Sprintf("/rest/default-reviewers/1.0/projects/%s/condition/%s",
url.PathEscape(projectKey),
url.PathEscape(conditionID),
)
}
return fmt.Sprintf("/rest/default-reviewers/1.0/projects/%s/repos/%s/condition/%s",
url.PathEscape(projectKey),
url.PathEscape(repositorySlug),
url.PathEscape(conditionID),
)
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
func resourceDefaultReviewersConditionCreate(d *schema.ResourceData, m interface{}) error {
projectKey := d.Get("project_key").(string)
repositorySlug := d.Get("repository_slug").(string)
condition := &DefaultReviewersConditionPayload{
SourceMatcher: expandMatcher(d.Get("source_matcher").(map[string]interface{})),
TargetMatcher: expandMatcher(d.Get("target_matcher").(map[string]interface{})),
Reviewers: expandReviewers(d.Get("reviewers").(*schema.Set)),
RequiredApprovals: d.Get("required_approvals").(int),
}
if contains(validMatcherTypeIDs, condition.SourceMatcher.Type.ID) == false {
return fmt.Errorf("source_matcher.type_id %s must be one of %v", condition.SourceMatcher.Type.ID, validMatcherTypeIDs)
}
if contains(validMatcherTypeIDs, condition.TargetMatcher.Type.ID) == false {
return fmt.Errorf("target_matcher.type_id %s must be one of %v", condition.TargetMatcher.Type.ID, validMatcherTypeIDs)
}
if condition.RequiredApprovals > len(condition.Reviewers) {
return fmt.Errorf("required_approvals %d cannot be more than length of reviewers %d", condition.RequiredApprovals, len(condition.Reviewers))
}
bytedata, err := json.Marshal(condition)
if err != nil {
return err
}
client := m.(*BitbucketServerProvider).BitbucketClient
resp, err := client.Post(getCreateConditionURI(projectKey, repositorySlug), bytes.NewBuffer(bytedata))
if err != nil {
return err
}
var newCondition DefaultReviewersConditionResp
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, &newCondition)
if err != nil {
return err
}
d.SetId(createResourceID(newCondition.ID, projectKey, repositorySlug))
return resourceDefaultReviewersConditionRead(d, m)
}
func resourceDefaultReviewersConditionRead(d *schema.ResourceData, m interface{}) error {
conditionID, projectKey, repositorySlug, err := parseResourceID(d.Id())
if err != nil {
return err
}
client := m.(*BitbucketServerProvider).BitbucketClient
resp, err := client.Get(getReadConditionURI(projectKey, repositorySlug))
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("unable to find a matching default reviewers condition %s. API returned %d", d.Id(), resp.StatusCode)
}
var conditions []DefaultReviewersConditionResp
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(body, &conditions)
if err != nil {
return err
}
for _, c := range conditions {
cID := strconv.Itoa(c.ID)
if cID == conditionID {
d.Set("project_key", projectKey)
d.Set("repository_slug", repositorySlug)
d.Set("source_matcher", collapseMatcher(refMatcherToMatcher(c.SourceRefMatcher)))
d.Set("target_matcher", collapseMatcher(refMatcherToMatcher(c.TargetRefMatcher)))
d.Set("reviewers", collapseReviewers(c.Reviewers))
d.Set("required_approvals", c.RequiredApprovals)
return nil
}
}
return fmt.Errorf("unable to find a matching default reviewers condition %s", d.Id())
}
func resourceDefaultReviewersConditionDelete(d *schema.ResourceData, m interface{}) error {
conditionID, projectKey, repositorySlug, err := parseResourceID(d.Id())
if err != nil {
return err
}
client := m.(*BitbucketServerProvider).BitbucketClient
_, err = client.Delete(getDeleteConditionURI(conditionID, projectKey, repositorySlug))
return err
}

View File

@@ -0,0 +1,149 @@
package bitbucket
import (
"fmt"
"math/rand"
"regexp"
"testing"
"time"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccBitbucketDefaultReviewersCondition_forProject(t *testing.T) {
key := fmt.Sprintf("%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int())
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckBitbucketDefaultReviewersConditionDestroy,
Steps: []resource.TestStep{
{
Config: testAccBitbucketDefaultReviewersConditionResourceForProject(key, 1),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "project_key", "TEST"+key),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "source_matcher.id", "any"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "source_matcher.type_id", "ANY_REF"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "target_matcher.id", "any"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "target_matcher.type_id", "ANY_REF"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "reviewers.#", "1"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "required_approvals", "1"),
),
},
},
})
}
func TestAccBitbucketDefaultReviewersCondition_forRepository(t *testing.T) {
key := fmt.Sprintf("%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int())
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckBitbucketDefaultReviewersConditionDestroy,
Steps: []resource.TestStep{
{
Config: testAccBitbucketDefaultReviewersConditionResourceForRepository(key),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "project_key", "TEST"+key),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "repository_slug", "test-repo-"+key),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "source_matcher.id", "any"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "source_matcher.type_id", "ANY_REF"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "target_matcher.id", "any"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "target_matcher.type_id", "ANY_REF"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "reviewers.#", "1"),
resource.TestCheckResourceAttr("bitbucketserver_default_reviewers_condition.test", "required_approvals", "1"),
),
},
{
ResourceName: "bitbucketserver_default_reviewers_condition.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func TestAccBitbucketDefaultReviewersCondition_expectRequiredApprovalsError(t *testing.T) {
key := fmt.Sprintf("%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int())
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccBitbucketDefaultReviewersConditionResourceForProject(key, 2),
ExpectError: regexp.MustCompile("required_approvals 2 cannot be more than length of reviewers 1"),
},
},
})
}
func testAccCheckBitbucketDefaultReviewersConditionDestroy(s *terraform.State) error {
_, ok := s.RootModule().Resources["bitbucketserver_default_reviewers_condition.test"]
if !ok {
return fmt.Errorf("not found %s", "bitbucketserver_default_reviewers_condition.test")
}
return nil
}
func testAccBitbucketDefaultReviewersConditionResourceForProject(key string, requiredApprovals int) string {
return fmt.Sprintf(`
resource "bitbucketserver_project" "test" {
key = "TEST%s"
name = "test-project-%s"
}
data "bitbucketserver_user" "reviewer" {
name = "admin"
}
resource "bitbucketserver_default_reviewers_condition" "test" {
project_key = bitbucketserver_project.test.key
source_matcher = {
id = "any"
type_id = "ANY_REF"
}
target_matcher = {
id = "any"
type_id = "ANY_REF"
}
reviewers = [data.bitbucketserver_user.reviewer.user_id]
required_approvals = %d
}`, key, key, requiredApprovals)
}
func testAccBitbucketDefaultReviewersConditionResourceForRepository(key string) string {
return fmt.Sprintf(`
resource "bitbucketserver_project" "test" {
key = "TEST%s"
name = "test-project-%s"
}
resource "bitbucketserver_repository" "test" {
project = bitbucketserver_project.test.key
name = "test-repo-%s"
}
data "bitbucketserver_user" "reviewer" {
name = "admin"
}
resource "bitbucketserver_default_reviewers_condition" "test" {
project_key = bitbucketserver_project.test.key
repository_slug = bitbucketserver_repository.test.slug
source_matcher = {
id = "any"
type_id = "ANY_REF"
}
target_matcher = {
id = "any"
type_id = "ANY_REF"
}
reviewers = [data.bitbucketserver_user.reviewer.user_id]
required_approvals = 1
}`, key, key, key)
}

View File

@@ -0,0 +1,52 @@
---
id: bitbucketserver_default_reviewers_condition
title: bitbucketserver_default_reviewers_condition
---
Create a default reviewers condition for project or repository.
## Example Usage
```hcl
resource "bitbucketserver_default_reviewers_condition" "condition" {
project_key = "PRO"
repository_slug = "repository-1"
source_matcher = {
id = "any"
type_id = "ANY_REF"
}
target_matcher = {
id = "any"
type_id = "ANY_REF"
}
reviewers = [1]
required_approvals = 1
}
```
## Argument Reference
* `project_key` - Required. Project key.
* `repository_slug` - Optional. Repository slug. If empty, default reviewers condition will be created for the whole project.
* `source_matcher.id` - Required. Source branch matcher id. It can be either `"any"` to match all branches, `"refs/heads/master"` to match certain branch, `"pattern"` to match multiple branches or `"development"` to match branching model.
* `source_matcher.type_id` - Required. Source branch matcher type.It must be one of: `"ANY_REF"`, `"BRANCH"`, `"PATTERN"`, `"MODEL_BRANCH"`.
* `target_matcher.id` - Required. Target branch matcher id. It can be either `"any"` to match all branches, `"refs/heads/master"` to match certain branch, `"pattern"` to match multiple branches or `"development"` to match branching model.
* `target_matcher.type_id` - Required. Target branch matcher type. It must be one of: `"ANY_REF"`, `"BRANCH"`, `"PATTERN"`, `"MODEL_BRANCH"`.
* `reviewers` - Required. IDs of Bitbucket users to become default reviewers when new pull request is created.
* `required_approvals` - Required. The number of default reviewers that must approve a pull request. Can't be higher than length of `reviewers`.
You can find more information about [how to use branch matchers here](https://confluence.atlassian.com/bitbucketserver/add-default-reviewers-to-pull-requests-834221295.html).
## Import
Import a default reviewers condition reference via the ID in this format `condition_id:project_key:repository_slug`.
```
terraform import bitbucketserver_default_reviewers_condition.test 1:pro:repo
```
When importing a default reviewers condition for the whole project omit the `repository_slug`.
```
terraform import bitbucketserver_default_reviewers_condition.test 1:pro
```

View File

@@ -52,6 +52,9 @@
"bitbucketserver_banner": {
"title": "bitbucketserver_banner"
},
"bitbucketserver_default_reviewers_condition": {
"title": "bitbucketserver_default_reviewers_condition"
},
"bitbucketserver_global_permissions_group": {
"title": "bitbucketserver_global_permissions_group"
},

View File

@@ -21,6 +21,7 @@
],
"Resources": [
"bitbucketserver_banner",
"bitbucketserver_default_reviewers_condition",
"bitbucketserver_global_permissions_group",
"bitbucketserver_global_permissions_user",
"bitbucketserver_group",