provider/github: Implements github_branch_protection (#10476)

This commit is contained in:
Andy Lindeman 2017-04-20 14:11:49 -04:00 committed by Paul Stack
parent 7925b76bad
commit 28d71e6624
5 changed files with 574 additions and 0 deletions

View File

@ -41,6 +41,7 @@ func Provider() terraform.ResourceProvider {
"github_organization_webhook": resourceGithubOrganizationWebhook(),
"github_repository_collaborator": resourceGithubRepositoryCollaborator(),
"github_issue_label": resourceGithubIssueLabel(),
"github_branch_protection": resourceGithubBranchProtection(),
},
ConfigureFunc: providerConfigure,

View File

@ -0,0 +1,278 @@
package github
import (
"context"
"errors"
"github.com/google/go-github/github"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceGithubBranchProtection() *schema.Resource {
return &schema.Resource{
Create: resourceGithubBranchProtectionCreate,
Read: resourceGithubBranchProtectionRead,
Update: resourceGithubBranchProtectionUpdate,
Delete: resourceGithubBranchProtectionDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"repository": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"branch": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"required_status_checks": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"include_admins": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"strict": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"contexts": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
},
},
"required_pull_request_reviews": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"include_admins": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
},
},
"restrictions": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"users": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"teams": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
},
},
},
}
}
func resourceGithubBranchProtectionCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Organization).client
r := d.Get("repository").(string)
b := d.Get("branch").(string)
protectionRequest, err := buildProtectionRequest(d)
if err != nil {
return err
}
_, _, err = client.Repositories.UpdateBranchProtection(context.TODO(), meta.(*Organization).name, r, b, protectionRequest)
if err != nil {
return err
}
d.SetId(buildTwoPartID(&r, &b))
return resourceGithubBranchProtectionRead(d, meta)
}
func resourceGithubBranchProtectionRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Organization).client
r, b := parseTwoPartID(d.Id())
githubProtection, _, err := client.Repositories.GetBranchProtection(context.TODO(), meta.(*Organization).name, r, b)
if err != nil {
d.SetId("")
return nil
}
d.Set("repository", r)
d.Set("branch", b)
rsc := githubProtection.RequiredStatusChecks
if rsc != nil {
d.Set("required_status_checks", []interface{}{
map[string]interface{}{
"include_admins": rsc.IncludeAdmins,
"strict": rsc.Strict,
"contexts": rsc.Contexts,
},
})
} else {
d.Set("required_status_checks", []interface{}{})
}
rprr := githubProtection.RequiredPullRequestReviews
if rprr != nil {
d.Set("required_pull_request_reviews", []interface{}{
map[string]interface{}{
"include_admins": rprr.IncludeAdmins,
},
})
} else {
d.Set("required_pull_request_reviews", []interface{}{})
}
restrictions := githubProtection.Restrictions
if restrictions != nil {
var userLogins []string
for _, u := range restrictions.Users {
if u.Login != nil {
userLogins = append(userLogins, *u.Login)
}
}
var teamSlugs []string
for _, t := range restrictions.Teams {
if t.Slug != nil {
teamSlugs = append(teamSlugs, *t.Slug)
}
}
d.Set("restrictions", []interface{}{
map[string]interface{}{
"users": userLogins,
"teams": teamSlugs,
},
})
} else {
d.Set("restrictions", []interface{}{})
}
return nil
}
func resourceGithubBranchProtectionUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Organization).client
r, b := parseTwoPartID(d.Id())
protectionRequest, err := buildProtectionRequest(d)
if err != nil {
return err
}
_, _, err = client.Repositories.UpdateBranchProtection(context.TODO(), meta.(*Organization).name, r, b, protectionRequest)
if err != nil {
return err
}
d.SetId(buildTwoPartID(&r, &b))
return resourceGithubBranchProtectionRead(d, meta)
}
func resourceGithubBranchProtectionDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Organization).client
r, b := parseTwoPartID(d.Id())
_, err := client.Repositories.RemoveBranchProtection(context.TODO(), meta.(*Organization).name, r, b)
return err
}
func buildProtectionRequest(d *schema.ResourceData) (*github.ProtectionRequest, error) {
protectionRequest := new(github.ProtectionRequest)
if v, ok := d.GetOk("required_status_checks"); ok {
vL := v.([]interface{})
if len(vL) > 1 {
return nil, errors.New("cannot specify required_status_checks more than one time")
}
for _, v := range vL {
m := v.(map[string]interface{})
rsc := new(github.RequiredStatusChecks)
rsc.IncludeAdmins = m["include_admins"].(bool)
rsc.Strict = m["strict"].(bool)
rsc.Contexts = []string{}
if contexts, ok := m["contexts"].([]interface{}); ok {
for _, c := range contexts {
rsc.Contexts = append(rsc.Contexts, c.(string))
}
}
protectionRequest.RequiredStatusChecks = rsc
}
}
if v, ok := d.GetOk("required_pull_request_reviews"); ok {
vL := v.([]interface{})
if len(vL) > 1 {
return nil, errors.New("cannot specify required_pull_request_reviews more than one time")
}
for _, v := range vL {
m := v.(map[string]interface{})
rprr := new(github.RequiredPullRequestReviews)
rprr.IncludeAdmins = m["include_admins"].(bool)
protectionRequest.RequiredPullRequestReviews = rprr
}
}
if v, ok := d.GetOk("restrictions"); ok {
vL := v.([]interface{})
if len(vL) > 1 {
return nil, errors.New("cannot specify restrictions more than one time")
}
for _, v := range vL {
m := v.(map[string]interface{})
restrictions := new(github.BranchRestrictionsRequest)
restrictions.Users = []string{}
if users, ok := m["users"].([]interface{}); ok {
for _, u := range users {
restrictions.Users = append(restrictions.Users, u.(string))
}
}
restrictions.Teams = []string{}
if teams, ok := m["teams"].([]interface{}); ok {
for _, t := range teams {
restrictions.Teams = append(restrictions.Teams, t.(string))
}
}
protectionRequest.Restrictions = restrictions
}
}
return protectionRequest, nil
}

View File

@ -0,0 +1,220 @@
package github
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/google/go-github/github"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccGithubBranchProtection_basic(t *testing.T) {
var protection github.Protection
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGithubBranchProtectionDestroy,
Steps: []resource.TestStep{
{
Config: testAccGithubBranchProtectionConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckGithubProtectedBranchExists("github_branch_protection.master", &protection),
testAccCheckGithubBranchProtectionRequiredStatusChecks(&protection, true, true, []string{"github/foo"}),
testAccCheckGithubBranchProtectionRestrictions(&protection, []string{testUser}, []string{}),
resource.TestCheckResourceAttr("github_branch_protection.master", "repository", testRepo),
resource.TestCheckResourceAttr("github_branch_protection.master", "branch", "master"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.include_admins", "true"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.strict", "true"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.contexts.#", "1"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.contexts.0", "github/foo"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_pull_request_reviews.0.include_admins", "true"),
resource.TestCheckResourceAttr("github_branch_protection.master", "restrictions.0.users.#", "1"),
resource.TestCheckResourceAttr("github_branch_protection.master", "restrictions.0.users.0", testUser),
resource.TestCheckResourceAttr("github_branch_protection.master", "restrictions.0.teams.#", "0"),
),
},
{
Config: testAccGithubBranchProtectionUpdateConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckGithubProtectedBranchExists("github_branch_protection.master", &protection),
testAccCheckGithubBranchProtectionRequiredStatusChecks(&protection, false, false, []string{"github/bar"}),
testAccCheckGithubBranchProtectionNoRestrictionsExist(&protection),
resource.TestCheckResourceAttr("github_branch_protection.master", "repository", testRepo),
resource.TestCheckResourceAttr("github_branch_protection.master", "branch", "master"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.include_admins", "false"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.strict", "false"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.contexts.#", "1"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_status_checks.0.contexts.0", "github/bar"),
resource.TestCheckResourceAttr("github_branch_protection.master", "required_pull_request_reviews.#", "0"),
resource.TestCheckResourceAttr("github_branch_protection.master", "restrictions.#", "0"),
),
},
},
})
}
func TestAccGithubBranchProtection_importBasic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccGithubBranchProtectionDestroy,
Steps: []resource.TestStep{
{
Config: testAccGithubBranchProtectionConfig,
},
{
ResourceName: "github_branch_protection.master",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func testAccCheckGithubProtectedBranchExists(n string, protection *github.Protection) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not Found: %s", n)
}
if rs.Primary.ID != "test-repo:master" {
return fmt.Errorf("Expected ID to be %v, got %v", "test-repo:master", rs.Primary.ID)
}
conn := testAccProvider.Meta().(*Organization).client
o := testAccProvider.Meta().(*Organization).name
r, b := parseTwoPartID(rs.Primary.ID)
githubProtection, _, err := conn.Repositories.GetBranchProtection(context.TODO(), o, r, b)
if err != nil {
return err
}
*protection = *githubProtection
return nil
}
}
func testAccCheckGithubBranchProtectionRequiredStatusChecks(protection *github.Protection, expectedIncludeAdmins bool, expectedStrict bool, expectedContexts []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rsc := protection.RequiredStatusChecks
if rsc == nil {
return fmt.Errorf("Expected RequiredStatusChecks to be present, but was nil")
}
if rsc.IncludeAdmins != expectedIncludeAdmins {
return fmt.Errorf("Expected RequiredStatusChecks.IncludeAdmins to be %v, got %v", expectedIncludeAdmins, rsc.IncludeAdmins)
}
if rsc.Strict != expectedStrict {
return fmt.Errorf("Expected RequiredStatusChecks.Strict to be %v, got %v", expectedStrict, rsc.Strict)
}
if !reflect.DeepEqual(rsc.Contexts, expectedContexts) {
return fmt.Errorf("Expected RequiredStatusChecks.Contexts to be %v, got %v", expectedContexts, rsc.Contexts)
}
return nil
}
}
func testAccCheckGithubBranchProtectionRestrictions(protection *github.Protection, expectedUserLogins []string, expectedTeamNames []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
restrictions := protection.Restrictions
if restrictions == nil {
return fmt.Errorf("Expected Restrictions to be present, but was nil")
}
userLogins := []string{}
for _, u := range restrictions.Users {
userLogins = append(userLogins, *u.Login)
}
if !reflect.DeepEqual(userLogins, expectedUserLogins) {
return fmt.Errorf("Expected Restrictions.Users to be %v, got %v", expectedUserLogins, userLogins)
}
teamLogins := []string{}
for _, t := range restrictions.Teams {
teamLogins = append(teamLogins, *t.Name)
}
if !reflect.DeepEqual(teamLogins, expectedTeamNames) {
return fmt.Errorf("Expected Restrictions.Teams to be %v, got %v", expectedTeamNames, teamLogins)
}
return nil
}
}
func testAccCheckGithubBranchProtectionNoRestrictionsExist(protection *github.Protection) resource.TestCheckFunc {
return func(s *terraform.State) error {
if protection.Restrictions != nil {
return fmt.Errorf("Expected Restrictions to be nil, but was %v", protection.Restrictions)
}
return nil
}
}
func testAccGithubBranchProtectionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*Organization).client
for _, rs := range s.RootModule().Resources {
if rs.Type != "github_branch_protection" {
continue
}
o := testAccProvider.Meta().(*Organization).name
r, b := parseTwoPartID(rs.Primary.ID)
protection, res, err := conn.Repositories.GetBranchProtection(context.TODO(), o, r, b)
if err == nil {
if protection != nil {
return fmt.Errorf("Branch protection still exists")
}
}
if res.StatusCode != 404 {
return err
}
return nil
}
return nil
}
var testAccGithubBranchProtectionConfig string = fmt.Sprintf(`
resource "github_branch_protection" "master" {
repository = "%s"
branch = "master"
required_status_checks = {
include_admins = true
strict = true
contexts = ["github/foo"]
}
required_pull_request_reviews {
include_admins = true
}
restrictions {
users = ["%s"]
}
}
`, testRepo, testUser)
var testAccGithubBranchProtectionUpdateConfig string = fmt.Sprintf(`
resource "github_branch_protection" "master" {
repository = "%s"
branch = "master"
required_status_checks = {
include_admins = false
strict = false
contexts = ["github/bar"]
}
}
`, testRepo)

View File

@ -0,0 +1,72 @@
---
layout: "github"
page_title: "GitHub: github_branch_protection"
sidebar_current: "docs-github-resource-branch-protection"
description: |-
Protects a GitHub branch.
---
# github\_branch\_protection
Protects a GitHub branch.
This resource allows you to configure branch protection for repositories in your organization. When applied, the branch will be protected from forced pushes and deletion. Additional constraints, such as required status checks or restrictions on users and teams, can also be configured.
## Example Usage
```
# Protect the master branch of the foo repository. Additionally, require that
# the "ci/travis" context to be passing and only allow the engineers team merge
# to the branch.
resource "github_branch_protection" "foo_master" {
repository = "foo"
branch = "master"
required_status_checks {
include_admins = true
strict = false
contexts = ["ci/travis"]
}
required_pull_request_reviews {
include_admins = true
}
restrictions {
teams = ["engineers"]
}
}
```
## Argument Reference
The following arguments are supported:
* `repository` - (Required) The GitHub repository name.
* `branch` - (Required) The Git branch to protect.
* `required_status_checks` - (Optional) Enforce restrictions for required status checks. See [Required Status Checks](#required-status-checks) below for details.
* `required_pull_request_reviews` - (Optional) Enforce restrictions for pull request reviews. See [Required Pull Request Reviews](#required-pull-request-reviews) below for details.
* `restrictions` - (Optional) Enforce restrictions for the users and teams that may push to the branch. See [Restrictions](#restrictions) below for details.
### Required Status Checks
`required_status_checks` supports the following arguments:
* `include_admins`: (Optional) Enforce required status checks for repository administrators. Defaults to `false`.
* `strict`: (Optional) Require branches to be up to date before merging. Defaults to `false`.
* `contexts`: (Optional) The list of status checks to require in order to merge into this branch. No status checks are required by default.
### Required Pull Request Reviews
`required_pull_request_reviews` supports the following arguments:
* `include_admins`: (Optional) Enforce required status checks for repository administrators. Defaults to `false`.
### Restrictions
`restrictions` supports the following arguments:
* `users`: (Optional) The list of user logins with push access.
* `teams`: (Optional) The list of team slugs with push access.
`restrictions` is only available for organization-owned repositories.

View File

@ -13,6 +13,9 @@
<li<%= sidebar_current("docs-github-resource") %>>
<a href="#">Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-github-resource-branch-protection") %>>
<a href="/docs/providers/github/r/branch_protection.html">github_branch_protection</a>
</li>
<li<%= sidebar_current("docs-github-resource-membership") %>>
<a href="/docs/providers/github/r/membership.html">github_membership</a>
</li>