diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85d9a531ef..e4c9d4886f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -156,6 +156,7 @@ export GITHUB_BASE_URL= export GITHUB_TEST_OWNER= export GITHUB_TEST_ORGANIZATION= export GITHUB_TEST_USER_TOKEN= +export GITHUB_TEST_PRE_RECEIVE_HOOK= ``` See [this project](https://github.com/terraformtesting/acceptance-tests) for more information on our old system for automated testing. diff --git a/github/provider.go b/github/provider.go index 8f44c95098..adc3ce875b 100644 --- a/github/provider.go +++ b/github/provider.go @@ -181,6 +181,7 @@ func Provider() *schema.Provider { "github_repository_milestone": resourceGithubRepositoryMilestone(), "github_repository_project": resourceGithubRepositoryProject(), "github_repository_pull_request": resourceGithubRepositoryPullRequest(), + "github_repository_pre_receive_hook": resourceGithubRepositoryPreReceiveHook(), "github_repository_ruleset": resourceGithubRepositoryRuleset(), "github_repository_topics": resourceGithubRepositoryTopics(), "github_repository_webhook": resourceGithubRepositoryWebhook(), diff --git a/github/provider_utils.go b/github/provider_utils.go index 2e3b44a095..42a7a821ad 100644 --- a/github/provider_utils.go +++ b/github/provider_utils.go @@ -74,6 +74,12 @@ func skipUnlessMode(t *testing.T, providerMode string) { t.Skipf("Skipping %s which requires %s mode", t.Name(), providerMode) } +func skipUnlessEnterpriseServer(t *testing.T) { + if os.Getenv("GITHUB_BASE_URL") == "" || os.Getenv("GITHUB_BASE_URL") == "https://api.github.com/" { + t.Skipf("Skipping %s which requires GitHub Enterprise Server", t.Name()) + } +} + func testAccCheckOrganization() error { baseURL := os.Getenv("GITHUB_BASE_URL") @@ -130,6 +136,13 @@ func testOwnerFunc() string { return owner } +func testPreReceiveHookFunc() string { + hookName := os.Getenv("GITHUB_TEST_PRE_RECEIVE_HOOK") + log.Printf("[INFO] Selecting pre-receive hook name '%s' from GITHUB_TEST_PRE_RECEIVE_HOOK environment variable", hookName) + + return hookName +} + const anonymous = "anonymous" const individual = "individual" const organization = "organization" diff --git a/github/resource_github_repository_pre_receive_hook.go b/github/resource_github_repository_pre_receive_hook.go new file mode 100644 index 0000000000..724dd5a58f --- /dev/null +++ b/github/resource_github_repository_pre_receive_hook.go @@ -0,0 +1,178 @@ +package github + +import ( + "context" + "fmt" + "strconv" + + "github.com/google/go-github/v66/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubRepositoryPreReceiveHook() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubRepositoryPreReceiveHookCreate, + Read: resourceGithubRepositoryPreReceiveHookRead, + Update: resourceGithubRepositoryPreReceiveHookUpdate, + Delete: resourceGithubRepositoryPreReceiveHookDelete, + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The repository of the pre-receive hook.", + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the pre-receive hook.", + }, + "enforcement": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{"enabled", "disabled", "testing"}, false), + Description: "The state of enforcement for the hook on the repository. Possible values for enforcement are 'enabled', 'disabled' and 'testing'. 'disabled' indicates the pre-receive hook will not run. 'enabled' indicates it will run and reject any pushes that result in a non-zero status. 'testing' means the script will run but will not cause any pushes to be rejected.", + }, + "configuration_url": { + Type: schema.TypeString, + Computed: true, + Description: "The URL for the endpoint where enforcement is set.", + }, + }, + } +} + +func resourceGithubRepositoryPreReceiveHookCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + + owner := meta.(*Owner).name + repoName := d.Get("repository").(string) + hookName := d.Get("name").(string) + + hook, err := fetchGitHubRepositoryPreReceiveHookByName(meta, repoName, hookName) + if err != nil { + return err + } + + enforcement := d.Get("enforcement").(string) + hook.Enforcement = &enforcement + + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + _, _, err = client.Repositories.UpdatePreReceiveHook(ctx, owner, repoName, hook.GetID(), hook) + if err != nil { + return err + } + d.SetId(strconv.FormatInt(hook.GetID(), 10)) + + return resourceGithubRepositoryPreReceiveHookRead(d, meta) +} + +func resourceGithubRepositoryPreReceiveHookRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + owner := meta.(*Owner).name + repoName := d.Get("repository").(string) + + hookID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + + hook, _, err := client.Repositories.GetPreReceiveHook(ctx, owner, repoName, hookID) + if err != nil { + return err + } + if err = d.Set("enforcement", hook.Enforcement); err != nil { + return err + } + if err = d.Set("configuration_url", hook.ConfigURL); err != nil { + return err + } + + return nil +} + +func resourceGithubRepositoryPreReceiveHookUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + owner := meta.(*Owner).name + repoName := d.Get("repository").(string) + enforcement := d.Get("enforcement").(string) + + hookID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + + hook := &github.PreReceiveHook{ + Enforcement: &enforcement, + } + _, _, err = client.Repositories.UpdatePreReceiveHook(ctx, owner, repoName, hookID, hook) + if err != nil { + return err + } + + return resourceGithubRepositoryPreReceiveHookRead(d, meta) +} + +func resourceGithubRepositoryPreReceiveHookDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Owner).v3client + ctx := context.WithValue(context.Background(), ctxId, d.Id()) + + owner := meta.(*Owner).name + repoName := d.Get("repository").(string) + + hookID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return unconvertibleIdErr(d.Id(), err) + } + _, err = client.Repositories.DeletePreReceiveHook(ctx, owner, repoName, hookID) + if err != nil { + return err + } + + return nil +} + +func fetchGitHubRepositoryPreReceiveHookByName(meta interface{}, repoName, hookName string) (*github.PreReceiveHook, error) { + ctx := context.Background() + client := meta.(*Owner).v3client + owner := meta.(*Owner).name + + opt := &github.ListOptions{ + PerPage: 100, + } + + var hook *github.PreReceiveHook + + for { + hooks, resp, err := client.Repositories.ListPreReceiveHooks(ctx, owner, repoName, opt) + if err != nil { + return nil, err + } + + for _, h := range hooks { + n := *h.Name + if n == hookName { + hook = h + break + } + } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage + } + + if *hook.ID <= 0 { + return nil, fmt.Errorf("no pre-receive hook with name %s found on %s/%s", hookName, owner, repoName) + } + + return hook, nil +} diff --git a/github/resource_github_repository_pre_receive_hook_test.go b/github/resource_github_repository_pre_receive_hook_test.go new file mode 100644 index 0000000000..b523237506 --- /dev/null +++ b/github/resource_github_repository_pre_receive_hook_test.go @@ -0,0 +1,59 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGithubRepositoryPreReceiveHook_basic(t *testing.T) { + skipUnlessEnterpriseServer(t) + + randString := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccGithubRepositoryPreReceiveHookConfig(randString, "enabled"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository_pre_receive_hook.test", "enforcement", "enabled"), + resource.TestCheckResourceAttrSet( + "github_repository_pre_receive_hook.test", "id"), + resource.TestCheckResourceAttrSet( + "github_repository_pre_receive_hook.test", "configuration_url"), + ), + }, + { + Config: testAccGithubRepositoryPreReceiveHookConfig(randString, "disabled"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository_pre_receive_hook.test", "enforcement", "disabled"), + resource.TestCheckResourceAttrSet( + "github_repository_pre_receive_hook.test", "id"), + resource.TestCheckResourceAttrSet( + "github_repository_pre_receive_hook.test", "configuration_url"), + ), + }, + }, + }) +} + +func testAccGithubRepositoryPreReceiveHookConfig(randString string, enforcement string) string { + return fmt.Sprintf(` +resource "github_repository" "test" { + name = "foo-%[1]s" + description = "Terraform acceptance tests" +} + +resource "github_repository_pre_receive_hook" "test" { + name = "%[2]s" + repository = github_repository.test.name + enforcement = "%[3]s" +} +`, randString, testPreReceiveHookFunc(), enforcement) +} diff --git a/website/docs/r/repository_pre_receive_hook.html.markdown b/website/docs/r/repository_pre_receive_hook.html.markdown new file mode 100644 index 0000000000..c6e05abf84 --- /dev/null +++ b/website/docs/r/repository_pre_receive_hook.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "github" +page_title: "GitHub: github_repository_pre_receive_hook" +description: |- + Creates and manages repository pre-receive hooks. +--- + +# github_repository_pre_receive_hook + +This resource allows you to create and manage pre-receive hooks for repositories. + +~> **Note** Repository pre-receive hooks are currently only available in GitHub Enterprise Server. + +## Example usage + +``` +resource "github_repository_pre_receive_hook" "example" { + repository = "test-repo" + name = "ensure-conventional-commits" + enforcement = "enabled" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `repository` - (Required) The repository of the pre-receive hook. + +* `name` - (Required) The name of the pre-receive hook. + +* `enforcement` - (Required) The state of enforcement for the hook on the repository. Possible values for enforcement are `enabled`, `disabled` and `testing`. `disabled` indicates the pre-receive hook will not run. `enabled` indicates it will run and reject any pushes that result in a non-zero status. `testing` means the script will run but will not cause any pushes to be rejected. + +## Attributes Reference + +The following additional attributes are exported: + +* `configuration_url` - The URL for the endpoint where enforcement is set. diff --git a/website/github.erb b/website/github.erb index 62cac71347..a9d7c88b44 100644 --- a/website/github.erb +++ b/website/github.erb @@ -343,6 +343,9 @@