diff --git a/bitbucket/provider.go b/bitbucket/provider.go index 1c3ac7a..f0764eb 100644 --- a/bitbucket/provider.go +++ b/bitbucket/provider.go @@ -61,6 +61,7 @@ func Provider() terraform.ResourceProvider { "bitbucketserver_project_hook": resourceProjectHook(), "bitbucketserver_project_permissions_group": resourceProjectPermissionsGroup(), "bitbucketserver_project_permissions_user": resourceProjectPermissionsUser(), + "bitbucketserver_pr_settings": resourcePrSettings(), "bitbucketserver_repository": resourceRepository(), "bitbucketserver_repository_hook": resourceRepositoryHook(), "bitbucketserver_repository_permissions_group": resourceRepositoryPermissionsGroup(), diff --git a/bitbucket/resource_pr_settings.go b/bitbucket/resource_pr_settings.go new file mode 100644 index 0000000..849b494 --- /dev/null +++ b/bitbucket/resource_pr_settings.go @@ -0,0 +1,261 @@ +package bitbucket + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +// The struct represents this JSON payload: +// https://docs.atlassian.com/bitbucket-server/rest/7.17.0/bitbucket-rest.html#idp375 +type PrSettings struct { + RequiredApprovers int `json:"requiredApprovers"` + RequiredSuccessfulBuilds int `json:"requiredSuccessfulBuilds"` + RequiredAllApprovers bool `json:"requiredAllApprovers,omitempty"` + RequiredAllTasksComplete bool `json:"requiredAllTasksComplete,omitempty"` + NoNeedsWork bool `json:"needsWork"` + MergeConfig MergeConfig `json:"mergeConfig,omitempty"` +} + +type MergeConfig struct { + DefaultStrategy MergeStrategy `json:"defaultStrategy,omitempty"` + EnabledStrategies []MergeStrategy `json:"strategies,omitempty"` + CommitSummaries int `json:"commitSummaries"` + Type string `json:"type,omitempty"` +} + +type MergeStrategy struct { + Id string `json:"id,omitempty"` + Flag string `json:"flag,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Enabled bool `json:"enabled,omitempty"` +} + +type DeleteMergeConfig struct { + MergeConfig struct { + } `json:"mergeConfig"` +} + +func resourcePrSettings() *schema.Resource { + return &schema.Resource{ + Create: resourcePrSettingsCreate, + Read: resourcePrSettingsRead, + Update: resourcePrSettingsCreate, + Delete: resourcePrSettingsDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "required_approvers": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "required_successful_builds": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "required_all_approvers": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "required_all_tasks_complete": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "no_needs_work_status": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "merge_config": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "default_strategy": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"no-ff", "ff", "ff-only", "rebase-no-ff", "rebase-ff-only", "squash", "squash-ff-only"}, false), + }, + "enabled_strategies": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + "commit_summaries": { + Type: schema.TypeInt, + Optional: true, + Default: 20, + }, + }, + }, + }, + }, + } +} + +func resourcePrSettingsCreate(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketServerProvider).BitbucketClient + settings := newPrSettingsFromResource(d) + + bytedata, err := json.Marshal(settings) + + if err != nil { + return err + } + + _, err = client.Post(fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/pull-requests", + d.Get("project").(string), + d.Get("repository").(string), + ), bytes.NewBuffer(bytedata)) + + if err != nil { + fmt.Println(err) + return err + } + + d.SetId(fmt.Sprintf("%s|%s", d.Get("project").(string), d.Get("repository").(string))) + + return resourcePrSettingsRead(d, m) +} + +func newPrSettingsFromResource(d *schema.ResourceData) *PrSettings { + settings := &PrSettings{ + RequiredApprovers: d.Get("required_approvers").(int), + RequiredSuccessfulBuilds: d.Get("required_successful_builds").(int), + RequiredAllApprovers: d.Get("required_all_approvers").(bool), + RequiredAllTasksComplete: d.Get("required_all_tasks_complete").(bool), + NoNeedsWork: d.Get("no_needs_work_status").(bool), + MergeConfig: expandMergeConfig(d.Get("merge_config").([]interface{})), + } + + return settings +} + +func resourcePrSettingsRead(d *schema.ResourceData, m interface{}) error { + id := d.Id() + + if id != "" { + idparts := strings.Split(id, "|") + if len(idparts) == 2 { + _ = d.Set("project", idparts[0]) + _ = d.Set("repository", idparts[1]) + } else { + return fmt.Errorf("incorrect ID format, should match `project|repository`") + } + } + + client := m.(*BitbucketServerProvider).BitbucketClient + req, err := client.Get(fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/pull-requests", + d.Get("project").(string), + d.Get("repository").(string), + )) + + if err != nil { + return err + } + + if req.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Workzone Reviewers object (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + var settings PrSettings + + body, readerr := ioutil.ReadAll(req.Body) + if readerr != nil { + return readerr + } + + decodeerr := json.Unmarshal(body, &settings) + if decodeerr != nil { + return decodeerr + } + + d.Set("required_approvers", settings.RequiredApprovers) + d.Set("required_successful_builds", settings.RequiredSuccessfulBuilds) + d.Set("required_all_approvers", settings.RequiredAllApprovers) + d.Set("required_all_tasks_complete", settings.RequiredAllTasksComplete) + d.Set("no_needs_work_status", settings.NoNeedsWork) + d.Set("merge_config", collapseMergeConfig(settings.MergeConfig)) + + return nil +} + +func resourcePrSettingsDelete(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketServerProvider).BitbucketClient + + project := d.Get("project").(string) + repository := d.Get("repository").(string) + + settings := &DeleteMergeConfig{} + + bytedata, err := json.Marshal(settings) + + if err != nil { + return err + } + + _, err = client.Post(fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/pull-requests", + url.QueryEscape(project), + url.QueryEscape(repository), + ), bytes.NewBuffer(bytedata)) + + return err +} + +func expandMergeConfig(l []interface{}) MergeConfig { + mergeConfigMap := l[0].(map[string]interface{}) + mergeConfig := MergeConfig{ + DefaultStrategy: MergeStrategy{ + Id: mergeConfigMap["default_strategy"].(string), + }, + CommitSummaries: mergeConfigMap["commit_summaries"].(int), + Type: "repository", + } + for _, item := range mergeConfigMap["enabled_strategies"].([]interface{}) { + strategy := MergeStrategy{ + Id: item.(string), + Enabled: true, + } + mergeConfig.EnabledStrategies = append(mergeConfig.EnabledStrategies, strategy) + } + return mergeConfig +} + +func collapseMergeConfig(rp MergeConfig) []interface{} { + + m := map[string]interface{}{ + "default_strategy": rp.DefaultStrategy.Id, + "commit_summaries": rp.CommitSummaries, + "enabled_strategies": rp.EnabledStrategies, + } + + return []interface{}{m} +} diff --git a/bitbucket/resource_pr_settings_test.go b/bitbucket/resource_pr_settings_test.go new file mode 100644 index 0000000..3a6d483 --- /dev/null +++ b/bitbucket/resource_pr_settings_test.go @@ -0,0 +1,96 @@ +package bitbucket + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccBitbucketResourcePrSettings_requiredArgumentsOnly(t *testing.T) { + projectKey := fmt.Sprintf("TEST%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int()) + + config := baseConfigForRepositoryBasedTests(projectKey) + ` + resource "bitbucketserver_pr_settings" "test" { + project = bitbucketserver_project.test.key + repository = bitbucketserver_repository.test.name + merge_config { + default_strategy = "no-ff" + enabled_strategies = ["no-ff"] + } + } + ` + resourceName := "bitbucketserver_pr_settings.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%v|repo", projectKey)), + resource.TestCheckResourceAttr(resourceName, "project", projectKey), + resource.TestCheckResourceAttr(resourceName, "repository", "repo"), + resource.TestCheckResourceAttr(resourceName, "merge_config.0.default_strategy", "no-ff"), + resource.TestCheckResourceAttr(resourceName, "merge_config.0.enabled_strategies.0", "no-ff"), + resource.TestCheckResourceAttr(resourceName, "merge_config.0.commit_summaries", "20"), + resource.TestCheckResourceAttr(resourceName, "no_needs_work_status", "false"), + resource.TestCheckResourceAttr(resourceName, "required_all_approvers", "false"), + resource.TestCheckResourceAttr(resourceName, "required_all_tasks_complete", "false"), + resource.TestCheckResourceAttr(resourceName, "required_approvers", "0"), + resource.TestCheckResourceAttr(resourceName, "required_successful_builds", "0"), + ), + }, + }, + }) +} + +func TestAccBitbucketResourcePrSettings_allArguments(t *testing.T) { + projectKey := fmt.Sprintf("TEST%v", rand.New(rand.NewSource(time.Now().UnixNano())).Int()) + + config := baseConfigForRepositoryBasedTests(projectKey) + ` + resource "bitbucketserver_pr_settings" "test" { + project = bitbucketserver_project.test.key + repository = bitbucketserver_repository.test.name + no_needs_work_status = true + required_all_approvers = true + required_all_tasks_complete = true + required_approvers = 1 + required_successful_builds = 1 + merge_config { + default_strategy = "no-ff" + enabled_strategies = ["no-ff", "ff"] + commit_summaries = 30 + } + } + ` + + resourceName := "bitbucketserver_pr_settings.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "id", fmt.Sprintf("%v|repo", projectKey)), + resource.TestCheckResourceAttr(resourceName, "project", projectKey), + resource.TestCheckResourceAttr(resourceName, "repository", "repo"), + resource.TestCheckResourceAttr(resourceName, "merge_config.0.default_strategy", "no-ff"), + resource.TestCheckResourceAttr(resourceName, "merge_config.0.enabled_strategies.0", "no-ff"), + resource.TestCheckResourceAttr(resourceName, "merge_config.0.enabled_strategies.1", "ff"), + resource.TestCheckResourceAttr(resourceName, "merge_config.0.commit_summaries", "30"), + resource.TestCheckResourceAttr(resourceName, "no_needs_work_status", "true"), + resource.TestCheckResourceAttr(resourceName, "required_all_approvers", "true"), + resource.TestCheckResourceAttr(resourceName, "required_all_tasks_complete", "true"), + resource.TestCheckResourceAttr(resourceName, "required_approvers", "1"), + resource.TestCheckResourceAttr(resourceName, "required_successful_builds", "1"), + ), + }, + }, + }) +} diff --git a/docs/resources/bitbucketserver_pr_settings.md b/docs/resources/bitbucketserver_pr_settings.md new file mode 100644 index 0000000..588803f --- /dev/null +++ b/docs/resources/bitbucketserver_pr_settings.md @@ -0,0 +1,35 @@ +# Resource: bitbucketserver_pr_settings + +Provides the ability to manage pull request settings for the context repository. + +## Example Usage + +```hcl +resource "bitbucketserver_pr_settings" "test" { + project = "MYPROJ" + repository = "repo" + no_needs_work_status = true + required_all_approvers = true + required_all_tasks_complete = true + required_approvers = 1 + required_successful_builds = 1 + merge_config { + default_strategy = "no-ff" + enabled_strategies = ["no-ff", "ff"] + commit_summaries = 30 + } +} +``` + +## Argument Reference + +* `project` - Required. Project Key that contains target repository. +* `repository` - Required. Repository slug of target repository. +* `merge_config.default_strategy` - Required. Default [merge strategy](https://confluence.atlassian.com/bitbucketserver0717/pull-request-merge-strategies-1087535782.html?utm_campaign=in-app-help&%3Butm_source=stash&%3Butm_medium=in-app-help). Git merge strategies affect the way the Git history appears after merging a pull request. Must be one of `no-ff`, `ff`, `ff-only`, `rebase-no-ff`, `rebase-ff-only`, `squash`, `squash-ff-only`. +* `merge_config.enabled_strategies` - Required. List of enabled merge strategies. Must contain at least the strategy that you specify as the default one. +* `merge_config.commit_summaries` - Optional. Controls the number of commit summaries included in commit messages for pull requests. Default `20`. +* `required_approvers` - Optional. The number of approvals required on a pull request for it to be mergeable. Default `0`. +* `required_successful_builds` - Optional. The number of successful builds on a pull request for it to be mergeable. Default `0`. +* `required_all_approvers` - Optional. Whether or not all approvers must approve a pull request for it to be mergeable. Default `false`. +* `required_all_tasks_complete` - Optional. Whether or not all tasks on a pull request need to be completed for it to be mergeable. Default `false`. +* `no_needs_work_status` - Optional. Whether or not to block the merge if any reviewers have marked the pull request as 'needs work'. Default `false`.