diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index 259325544..0e21f061e 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -33,7 +33,7 @@ The following attributes are exported: * `path_with_namespace` - The path of the repository with namespace. * `namespace_id` - The namespace (group or user) of the project. Defaults to your user. - See [`gitlab_group`](../r/group.html) for an example. + See [`gitlab_group`](../resources/group) for an example. * `description` - A description of the project. @@ -71,4 +71,36 @@ The following attributes are exported: * `remove_source_branch_after_merge` - Enable `Delete source branch` option by default for all new merge requests -* `packages_enabled` - Enable packages repository for the project. \ No newline at end of file +* `packages_enabled` - Enable packages repository for the project. + +* `push_rules` Push rules for the project (documented below). + +## Nested Blocks + +### push_rules + +For information on push rules, consult the [GitLab documentation](https://docs.gitlab.com/ce/push_rules/push_rules.html#push-rules). + +#### Attributes + +* `author_email_regex` - All commit author emails must match this regex, e.g. `@my-company.com$`. + +* `branch_name_regex` - All branch names must match this regex, e.g. `(feature|hotfix)\/*`. + +* `commit_message_regex` - All commit messages must match this regex, e.g. `Fixed \d+\..*`. + +* `commit_message_negative_regex` - No commit message is allowed to match this regex, for example `ssh\:\/\/`. + +* `file_name_regex` - All commited filenames must not match this regex, e.g. `(jar|exe)$`. + +* `commit_committer_check` - Users can only push commits to this repository that were committed with one of their own verified emails. + +* `deny_delete_tag` - Deny deleting a tag. + +* `member_check` - Restrict commits by author (email) to existing GitLab users. + +* `prevent_secrets` - GitLab will reject any files that are likely to contain secrets. + +* `reject_unsigned_commits` - Reject commit when it’s not signed through GPG. + +* `max_file_size` - Maximum file size (MB). diff --git a/docs/resources/project.md b/docs/resources/project.md index 971e434a4..758a3a7e5 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -80,6 +80,8 @@ The following arguments are supported: * `packages_enabled` - (Optional) Enable packages repository for the project. +* `push_rules` (Optional) Push rules for the project (documented below). + ## Attributes Reference The following additional attributes are exported: @@ -100,6 +102,35 @@ The following additional attributes are exported: * `remove_source_branch_after_merge` - Enable `Delete source branch` option by default for all new merge requests. +## Nested Blocks + +### push_rules + +For information on push rules, consult the [GitLab documentation](https://docs.gitlab.com/ce/push_rules/push_rules.html#push-rules). + +#### Arguments + +* `author_email_regex` - (Optional) All commit author emails must match this regex, e.g. `@my-company.com$`. + +* `branch_name_regex` - (Optional) All branch names must match this regex, e.g. `(feature|hotfix)\/*`. + +* `commit_message_regex` - (Optional) All commit messages must match this regex, e.g. `Fixed \d+\..*`. + +* `commit_message_negative_regex` - (Optional) No commit message is allowed to match this regex, for example `ssh\:\/\/`. + +* `file_name_regex` - (Optional) All commited filenames must not match this regex, e.g. `(jar|exe)$`. + +* `commit_committer_check` - (Optional, bool) Users can only push commits to this repository that were committed with one of their own verified emails. + +* `deny_delete_tag` - (Optional, bool) Deny deleting a tag. + +* `member_check` - (Optional, bool) Restrict commits by author (email) to existing GitLab users. + +* `prevent_secrets` - (Optional, bool) GitLab will reject any files that are likely to contain secrets. + +* `reject_unsigned_commits` - (Optional, bool) Reject commit when it’s not signed through GPG. + +* `max_file_size` - (Optional, int) Maximum file size (MB). ## Importing projects diff --git a/docs/resources/project_push_rules.md b/docs/resources/project_push_rules.md deleted file mode 100644 index 0dda2f7a1..000000000 --- a/docs/resources/project_push_rules.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -layout: "gitlab" -page_title: "GitLab: gitlab_project_push_rules" -sidebar_current: "docs-gitlab-resource-project-push-rules" -description: |- - Creates and manages push rules for GitLab projects ---- - -# gitlab\_project\_push\_rules - -This resource allows you to create and manage push rules for your GitLab projects. -For further information on push rules, consult the [gitlab -documentation](https://docs.gitlab.com/ce/push_rules/push_rules.html#push-rules). - -## Example Usage - -```hcl -resource "gitlab_project_push_rules" "example" { - commit_message_regex = "^(feat|feature|fix|chore|docs|BREAKING_CHANGE):.*" - prevent_secrets = true - branch_name_regex = "^PROJ-\d+-.*" - author_email_regex = "@my-company.com$" - commit_committer_check = true -} -``` - -## Argument Reference - -The following arguments are supported: - -* `project` - (Required, string) The name or id of the project to add the push rules to. - -* `commit_message_regex` - (Optional, string) All commit messages must match this regex, e.g. "Fixed \d+\..*" - -* `deny_delete_tag` - (Optional, bool) Deny deleting a tag - -* `member_check` - (Optional, bool) Restrict commits by author (email) to existing GitLab users - -* `prevent_secrets` - (Optional, bool) GitLab will reject any files that are likely to contain secrets - -* `branch_name_regex` - (Optional, string) All branch names must match this regex, e.g. "(feature|hotfix)\/*" - -* `author_email_regex` - (Optional, string) All commit author emails must match this regex, e.g. "@my-company.com$" - -* `file_name_regex` - (Optional, string) All commited filenames must not match this regex, e.g. "(jar|exe)$" - -* `max_file_size` - (Optional, int) Maximum file size (MB) - -* `commit_committer_check` - (Optional, bool) Users can only push commits to this repository that were committed with one of their own verified emails - -## Attributes Reference - -The resource exports the following attributes: - -* `id` - The unique id assigned to the push rules by the GitLab server. - -## Import - -Project push rules can be imported using the ID of the project or NAMESPACE/PROJECT_NAME, e.g. - -``` -$ terraform import gitlab_project_push_rules.example richardc/example -``` diff --git a/gitlab/data_source_gitlab_project.go b/gitlab/data_source_gitlab_project.go index 3213a6d32..2391b5d3e 100644 --- a/gitlab/data_source_gitlab_project.go +++ b/gitlab/data_source_gitlab_project.go @@ -1,8 +1,10 @@ package gitlab import ( + "errors" "fmt" "log" + "net/http" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/xanzy/go-gitlab" @@ -19,12 +21,10 @@ func dataSourceGitlabProject() *schema.Resource { }, "name": { Type: schema.TypeString, - Optional: true, Computed: true, }, "path": { Type: schema.TypeString, - Optional: true, Computed: true, }, "path_with_namespace": { @@ -33,89 +33,125 @@ func dataSourceGitlabProject() *schema.Resource { }, "description": { Type: schema.TypeString, - Optional: true, Computed: true, }, "default_branch": { Type: schema.TypeString, - Optional: true, Computed: true, }, "request_access_enabled": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "issues_enabled": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "merge_requests_enabled": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "pipelines_enabled": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "wiki_enabled": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "snippets_enabled": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "lfs_enabled": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "visibility_level": { Type: schema.TypeString, - Optional: true, Computed: true, }, "namespace_id": { Type: schema.TypeInt, - Optional: true, Computed: true, }, "ssh_url_to_repo": { Type: schema.TypeString, - Optional: true, Computed: true, }, "http_url_to_repo": { Type: schema.TypeString, - Optional: true, Computed: true, }, "web_url": { Type: schema.TypeString, - Optional: true, Computed: true, }, "runners_token": { Type: schema.TypeString, - Optional: true, Computed: true, }, "archived": { Type: schema.TypeBool, - Optional: true, Computed: true, }, "remove_source_branch_after_merge": { Type: schema.TypeBool, - Optional: true, Computed: true, }, + "push_rules": { + Type: schema.TypeList, + MaxItems: 1, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "author_email_regex": { + Type: schema.TypeString, + Computed: true, + }, + "branch_name_regex": { + Type: schema.TypeString, + Computed: true, + }, + "commit_message_regex": { + Type: schema.TypeString, + Computed: true, + }, + "commit_message_negative_regex": { + Type: schema.TypeString, + Computed: true, + }, + "file_name_regex": { + Type: schema.TypeString, + Computed: true, + }, + "commit_committer_check": { + Type: schema.TypeBool, + Computed: true, + }, + "deny_delete_tag": { + Type: schema.TypeBool, + Computed: true, + }, + "member_check": { + Type: schema.TypeBool, + Computed: true, + }, + "prevent_secrets": { + Type: schema.TypeBool, + Computed: true, + }, + "reject_unsigned_commits": { + Type: schema.TypeBool, + Computed: true, + }, + "max_file_size": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, }, } } @@ -152,5 +188,18 @@ func dataSourceGitlabProjectRead(d *schema.ResourceData, meta interface{}) error d.Set("runners_token", found.RunnersToken) d.Set("archived", found.Archived) d.Set("remove_source_branch_after_merge", found.RemoveSourceBranchAfterMerge) + + log.Printf("[DEBUG] Reading Gitlab project %q push rules", d.Id()) + + pushRules, _, err := client.Projects.GetProjectPushRules(d.Id()) + var httpError *gitlab.ErrorResponse + if errors.As(err, &httpError) && httpError.Response.StatusCode == http.StatusNotFound { + log.Printf("[DEBUG] Failed to get push rules for project %q: %v", d.Id(), err) + } else if err != nil { + return fmt.Errorf("Failed to get push rules for project %q: %w", d.Id(), err) + } + + d.Set("push_rules", flattenProjectPushRules(pushRules)) + return nil } diff --git a/gitlab/data_source_gitlab_project_test.go b/gitlab/data_source_gitlab_project_test.go index d7795ed29..37a7282e6 100644 --- a/gitlab/data_source_gitlab_project_test.go +++ b/gitlab/data_source_gitlab_project_test.go @@ -18,29 +18,32 @@ func TestAccDataGitlabProject_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccDataGitlabProjectConfig(projectname), - Check: resource.ComposeTestCheckFunc( - testAccDataSourceGitlabProject("gitlab_project.test", "data.gitlab_project.foo"), - ), + Check: testAccDataSourceGitlabProject("gitlab_project.test", "data.gitlab_project.foo", + []string{"id", "name", "path", "visibility", "description"}), + }, + { + SkipFunc: isRunningInCE, + Config: testAccDataGitlabProjectConfigPushRules(projectname), + Check: testAccDataSourceGitlabProject("gitlab_project.test", "data.gitlab_project.foo", + []string{"push_rules.0.author_email_regex"}), }, }, }) } -func testAccDataSourceGitlabProject(src, n string) resource.TestCheckFunc { +func testAccDataSourceGitlabProject(resourceName, dataSourceName string, testAttributes []string) resource.TestCheckFunc { return func(s *terraform.State) error { - project := s.RootModule().Resources[src] + project := s.RootModule().Resources[resourceName] projectResource := project.Primary.Attributes - search := s.RootModule().Resources[n] + search := s.RootModule().Resources[dataSourceName] searchResource := search.Primary.Attributes if searchResource["id"] == "" { return fmt.Errorf("Expected to get a project ID from Gitlab") } - testAttributes := []string{"id", "Name", "Path", "Visibility", "Description"} - for _, attribute := range testAttributes { if searchResource[attribute] != projectResource[attribute] { return fmt.Errorf("Expected the project %s to be: %s, but got: %s", attribute, projectResource[attribute], searchResource[attribute]) @@ -64,3 +67,21 @@ data "gitlab_project" "foo" { } `, projectname, projectname) } + +func testAccDataGitlabProjectConfigPushRules(projectName string) string { + return fmt.Sprintf(` +resource "gitlab_project" "test"{ + name = "%[1]s" + path = "%[1]s" + description = "Terraform acceptance tests" + visibility_level = "public" + push_rules { + author_email_regex = "foo" + } +} + +data "gitlab_project" "foo" { + id = gitlab_project.test.id +} + `, projectName) +} diff --git a/gitlab/provider.go b/gitlab/provider.go index fb30d8e5b..1bba6e8e0 100644 --- a/gitlab/provider.go +++ b/gitlab/provider.go @@ -73,7 +73,6 @@ func Provider() terraform.ResourceProvider { "gitlab_pipeline_schedule_variable": resourceGitlabPipelineScheduleVariable(), "gitlab_pipeline_trigger": resourceGitlabPipelineTrigger(), "gitlab_project_hook": resourceGitlabProjectHook(), - "gitlab_project_push_rules": resourceGitlabProjectPushRules(), "gitlab_deploy_key": resourceGitlabDeployKey(), "gitlab_deploy_key_enable": resourceGitlabDeployEnableKey(), "gitlab_deploy_token": resourceGitlabDeployToken(), diff --git a/gitlab/resource_gitlab_project.go b/gitlab/resource_gitlab_project.go index fe805c6e5..9acf6d4ec 100644 --- a/gitlab/resource_gitlab_project.go +++ b/gitlab/resource_gitlab_project.go @@ -1,8 +1,10 @@ package gitlab import ( + "errors" "fmt" "log" + "net/http" "time" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" @@ -191,6 +193,61 @@ var resourceGitLabProjectSchema = map[string]*schema.Schema{ Optional: true, Default: true, }, + "push_rules": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "author_email_regex": { + Type: schema.TypeString, + Optional: true, + }, + "branch_name_regex": { + Type: schema.TypeString, + Optional: true, + }, + "commit_message_regex": { + Type: schema.TypeString, + Optional: true, + }, + "commit_message_negative_regex": { + Type: schema.TypeString, + Optional: true, + }, + "file_name_regex": { + Type: schema.TypeString, + Optional: true, + }, + "commit_committer_check": { + Type: schema.TypeBool, + Optional: true, + }, + "deny_delete_tag": { + Type: schema.TypeBool, + Optional: true, + }, + "member_check": { + Type: schema.TypeBool, + Optional: true, + }, + "prevent_secrets": { + Type: schema.TypeBool, + Optional: true, + }, + "reject_unsigned_commits": { + Type: schema.TypeBool, + Optional: true, + }, + "max_file_size": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + }, + }, + }, + }, } func resourceGitlabProject() *schema.Resource { @@ -325,6 +382,18 @@ func resourceGitlabProjectCreate(d *schema.ResourceData, meta interface{}) error } } + if v, ok := d.GetOk("push_rules"); ok { + err := editOrAddPushRules(client, d.Id(), v.([]interface{})[0].(map[string]interface{})) + var httpError *gitlab.ErrorResponse + if errors.As(err, &httpError) && httpError.Response.StatusCode == http.StatusNotFound { + log.Printf("[DEBUG] Failed to edit push rules for project %q: %v", d.Id(), err) + return errors.New("Project push rules are not supported in your version of GitLab") + } + if err != nil { + return fmt.Errorf("Failed to edit push rules for project %q: %w", d.Id(), err) + } + } + return resourceGitlabProjectRead(d, meta) } @@ -343,6 +412,19 @@ func resourceGitlabProjectRead(d *schema.ResourceData, meta interface{}) error { } resourceGitlabProjectSetToState(d, project) + + log.Printf("[DEBUG] read gitlab project %q push rules", d.Id()) + + pushRules, _, err := client.Projects.GetProjectPushRules(d.Id()) + var httpError *gitlab.ErrorResponse + if errors.As(err, &httpError) && httpError.Response.StatusCode == http.StatusNotFound { + log.Printf("[DEBUG] Failed to get push rules for project %q: %v", d.Id(), err) + } else if err != nil { + return fmt.Errorf("Failed to get push rules for project %q: %w", d.Id(), err) + } + + d.Set("push_rules", flattenProjectPushRules(pushRules)) + return nil } @@ -468,6 +550,18 @@ func resourceGitlabProjectUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("push_rules") { + err := editOrAddPushRules(client, d.Id(), d.Get("push_rules").([]interface{})[0].(map[string]interface{})) + var httpError *gitlab.ErrorResponse + if errors.As(err, &httpError) && httpError.Response.StatusCode == http.StatusNotFound { + log.Printf("[DEBUG] Failed to get push rules for project %q: %v", d.Id(), err) + return errors.New("Project push rules are not supported in your version of GitLab") + } + if err != nil { + return fmt.Errorf("Failed to edit push rules for project %q: %w", d.Id(), err) + } + } + return resourceGitlabProjectRead(d, meta) } @@ -512,3 +606,85 @@ func resourceGitlabProjectDelete(d *schema.ResourceData, meta interface{}) error } return nil } + +func editOrAddPushRules(client *gitlab.Client, projectID string, m map[string]interface{}) error { + log.Printf("[DEBUG] Editing push rules for project %q", projectID) + + editOptions := expandEditProjectPushRuleOptions(m) + _, _, err := client.Projects.EditProjectPushRule(projectID, editOptions) + if err == nil { + return nil + } + + var httpErr *gitlab.ErrorResponse + if !errors.As(err, &httpErr) || httpErr.Response.StatusCode != http.StatusNotFound { + return err + } + + // A 404 could mean that the push rules need to be re-created. + + log.Printf("[DEBUG] Failed to edit push rules for project %q: %v", projectID, err) + log.Printf("[DEBUG] Creating new push rules for project %q", projectID) + + addOptions := expandAddProjectPushRuleOptions(m) + _, _, err = client.Projects.AddProjectPushRule(projectID, addOptions) + if err != nil { + return err + } + + return nil +} + +func expandEditProjectPushRuleOptions(m map[string]interface{}) *gitlab.EditProjectPushRuleOptions { + return &gitlab.EditProjectPushRuleOptions{ + AuthorEmailRegex: gitlab.String(m["author_email_regex"].(string)), + BranchNameRegex: gitlab.String(m["branch_name_regex"].(string)), + CommitMessageRegex: gitlab.String(m["commit_message_regex"].(string)), + CommitMessageNegativeRegex: gitlab.String(m["commit_message_negative_regex"].(string)), + FileNameRegex: gitlab.String(m["file_name_regex"].(string)), + CommitCommitterCheck: gitlab.Bool(m["commit_committer_check"].(bool)), + DenyDeleteTag: gitlab.Bool(m["deny_delete_tag"].(bool)), + MemberCheck: gitlab.Bool(m["member_check"].(bool)), + PreventSecrets: gitlab.Bool(m["prevent_secrets"].(bool)), + RejectUnsignedCommits: gitlab.Bool(m["reject_unsigned_commits"].(bool)), + MaxFileSize: gitlab.Int(m["max_file_size"].(int)), + } +} + +func expandAddProjectPushRuleOptions(m map[string]interface{}) *gitlab.AddProjectPushRuleOptions { + return &gitlab.AddProjectPushRuleOptions{ + AuthorEmailRegex: gitlab.String(m["author_email_regex"].(string)), + BranchNameRegex: gitlab.String(m["branch_name_regex"].(string)), + CommitMessageRegex: gitlab.String(m["commit_message_regex"].(string)), + CommitMessageNegativeRegex: gitlab.String(m["commit_message_negative_regex"].(string)), + FileNameRegex: gitlab.String(m["file_name_regex"].(string)), + CommitCommitterCheck: gitlab.Bool(m["commit_committer_check"].(bool)), + DenyDeleteTag: gitlab.Bool(m["deny_delete_tag"].(bool)), + MemberCheck: gitlab.Bool(m["member_check"].(bool)), + PreventSecrets: gitlab.Bool(m["prevent_secrets"].(bool)), + RejectUnsignedCommits: gitlab.Bool(m["reject_unsigned_commits"].(bool)), + MaxFileSize: gitlab.Int(m["max_file_size"].(int)), + } +} + +func flattenProjectPushRules(pushRules *gitlab.ProjectPushRules) (values []map[string]interface{}) { + if pushRules == nil { + return []map[string]interface{}{} + } + + return []map[string]interface{}{ + { + "author_email_regex": pushRules.AuthorEmailRegex, + "branch_name_regex": pushRules.BranchNameRegex, + "commit_message_regex": pushRules.CommitMessageRegex, + "commit_message_negative_regex": pushRules.CommitMessageNegativeRegex, + "file_name_regex": pushRules.FileNameRegex, + "commit_committer_check": pushRules.CommitCommitterCheck, + "deny_delete_tag": pushRules.DenyDeleteTag, + "member_check": pushRules.MemberCheck, + "prevent_secrets": pushRules.PreventSecrets, + "reject_unsigned_commits": pushRules.RejectUnsignedCommits, + "max_file_size": pushRules.MaxFileSize, + }, + } +} diff --git a/gitlab/resource_gitlab_project_push_rules.go b/gitlab/resource_gitlab_project_push_rules.go deleted file mode 100644 index 01109244f..000000000 --- a/gitlab/resource_gitlab_project_push_rules.go +++ /dev/null @@ -1,159 +0,0 @@ -package gitlab - -import ( - "fmt" - "log" - "strconv" - - "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - gitlab "github.com/xanzy/go-gitlab" -) - -func resourceGitlabProjectPushRules() *schema.Resource { - return &schema.Resource{ - Create: resourceGitlabProjectPushRulesCreate, - Read: resourceGitlabProjectPushRulesRead, - Update: resourceGitlabProjectPushRulesUpdate, - Delete: resourceGitlabProjectPushRulesDelete, - Importer: &schema.ResourceImporter{ - State: resourceGitlabProjectPushRulesImporter, - }, - Schema: map[string]*schema.Schema{ - "project": { - Type: schema.TypeString, - Required: true, - }, - "commit_message_regex": { - Type: schema.TypeString, - Optional: true, - }, - /* Not implemented in gitlab client - "commit_message_negative_regex": { - Type: schema.TypeString, - Optional: true, - }, - */ - "branch_name_regex": { - Type: schema.TypeString, - Optional: true, - }, - "author_email_regex": { - Type: schema.TypeString, - Optional: true, - }, - "file_name_regex": { - Type: schema.TypeString, - Optional: true, - }, - "deny_delete_tag": { - Type: schema.TypeBool, - Optional: true, - }, - "member_check": { - Type: schema.TypeBool, - Optional: true, - }, - "prevent_secrets": { - Type: schema.TypeBool, - Optional: true, - }, - "max_file_size": { - Type: schema.TypeInt, - Optional: true, - }, - }, - } -} -func resourceGitlabProjectPushRulesUpdate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gitlab.Client) - project := d.Get("project").(string) - options := &gitlab.EditProjectPushRuleOptions{ - CommitMessageRegex: gitlab.String(d.Get("commit_message_regex").(string)), - BranchNameRegex: gitlab.String(d.Get("branch_name_regex").(string)), - AuthorEmailRegex: gitlab.String(d.Get("author_email_regex").(string)), - FileNameRegex: gitlab.String(d.Get("file_name_regex").(string)), - DenyDeleteTag: gitlab.Bool(d.Get("deny_delete_tag").(bool)), - MemberCheck: gitlab.Bool(d.Get("member_check").(bool)), - PreventSecrets: gitlab.Bool(d.Get("prevent_secrets").(bool)), - MaxFileSize: gitlab.Int(d.Get("max_file_size").(int)), - } - log.Printf("[DEBUG] update gitlab project %s push rules %#v", project, *options) - _, _, err := client.Projects.EditProjectPushRule(project, options) - if err != nil { - return err - } - return resourceGitlabProjectPushRulesRead(d, meta) -} - -func resourceGitlabProjectPushRulesCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gitlab.Client) - project := d.Get("project").(string) - options := &gitlab.AddProjectPushRuleOptions{ - CommitMessageRegex: gitlab.String(d.Get("commit_message_regex").(string)), - BranchNameRegex: gitlab.String(d.Get("branch_name_regex").(string)), - AuthorEmailRegex: gitlab.String(d.Get("author_email_regex").(string)), - FileNameRegex: gitlab.String(d.Get("file_name_regex").(string)), - DenyDeleteTag: gitlab.Bool(d.Get("deny_delete_tag").(bool)), - MemberCheck: gitlab.Bool(d.Get("member_check").(bool)), - PreventSecrets: gitlab.Bool(d.Get("prevent_secrets").(bool)), - MaxFileSize: gitlab.Int(d.Get("max_file_size").(int)), - } - log.Printf("[DEBUG] create gitlab project %s push rules %#v", project, *options) - - pushRules, _, err := client.Projects.AddProjectPushRule(project, options) - if err != nil { - return err - } - d.SetId(fmt.Sprintf("%d", pushRules.ID)) - return resourceGitlabProjectPushRulesRead(d, meta) -} - -func resourceGitlabProjectPushRulesRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gitlab.Client) - project := d.Get("project").(string) - log.Printf("[DEBUG] read gitlab project %s push rules", project) - pushRules, _, err := client.Projects.GetProjectPushRules(project) - if err != nil { - return err - } - d.Set("commit_message_regex", pushRules.CommitMessageRegex) - d.Set("branch_name_regex", pushRules.BranchNameRegex) - d.Set("author_email_regex", pushRules.AuthorEmailRegex) - d.Set("file_name_regex", pushRules.FileNameRegex) - d.Set("deny_delete_tag", pushRules.DenyDeleteTag) - d.Set("member_check", pushRules.MemberCheck) - d.Set("prevent_secrets", pushRules.PreventSecrets) - d.Set("max_file_size", pushRules.MaxFileSize) - return nil -} - -func resourceGitlabProjectPushRulesDelete(d *schema.ResourceData, meta interface{}) error { - client := meta.(*gitlab.Client) - project := d.Get("project").(string) - log.Printf("[DEBUG] Delete gitlab project %s push rules", project) - log.Println(project) - _, err := client.Projects.DeleteProjectPushRule(project) - return err -} - -func resourceGitlabProjectPushRulesImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - // Push rule IDs in GitLab are internal identifiers. For ease of use, we allow importing using the project ID instead. - // This means we need to lookup the push rule ID during import. - - client := meta.(*gitlab.Client) - project := d.Id() - - log.Printf("[DEBUG] read gitlab project %s push rules", project) - - pushRules, _, err := client.Projects.GetProjectPushRules(project) - if err != nil { - return nil, err - } - - d.SetId(fmt.Sprintf("%d", pushRules.ID)) - - // Since project is used as a primary key in the Read function, we set that too. - d.Set("project", strconv.Itoa(pushRules.ProjectID)) - - return []*schema.ResourceData{d}, nil -} diff --git a/gitlab/resource_gitlab_project_push_rules_test.go b/gitlab/resource_gitlab_project_push_rules_test.go deleted file mode 100644 index a8ce22a45..000000000 --- a/gitlab/resource_gitlab_project_push_rules_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package gitlab - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/terraform" - gitlab "github.com/xanzy/go-gitlab" - - "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" -) - -func TestAccGitlabProjectPushRules_basic(t *testing.T) { - var pushRules gitlab.ProjectPushRules - rInt := acctest.RandInt() - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckGitlabProjectPushRulesDestroy, - Steps: []resource.TestStep{ - // Create project and push rules with basic options - { - SkipFunc: isRunningInCE, - Config: testAccGitlabProjectPushRulesConfig(rInt), - Check: resource.ComposeTestCheckFunc( - testAccCheckGitlabProjectPushRulesExists("gitlab_project_push_rules.foo", &pushRules), - testAccCheckGitlabProjectPushRulesAttributes(&pushRules, &testAccGitlabProjectPushRulesExpectedAttributes{ - CommitMessageRegex: "^(foo|bar).*", - BranchNameRegex: "^(foo|bar).*", - AuthorEmailRegex: "^(foo|bar).*", - FileNameRegex: "^(foo|bar).*", - DenyDeleteTag: true, - MemberCheck: true, - PreventSecrets: true, - MaxFileSize: 10, - }), - ), - }, - // Update the project push rules - { - SkipFunc: isRunningInCE, - Config: testAccGitlabProjectPushRulesUpdate(rInt), - Check: resource.ComposeTestCheckFunc( - testAccCheckGitlabProjectPushRulesExists("gitlab_project_push_rules.foo", &pushRules), - testAccCheckGitlabProjectPushRulesAttributes(&pushRules, &testAccGitlabProjectPushRulesExpectedAttributes{ - CommitMessageRegex: "^(fu|baz).*", - BranchNameRegex: "^(fu|baz).*", - AuthorEmailRegex: "^(fu|baz).*", - FileNameRegex: "^(fu|baz).*", - DenyDeleteTag: false, - MemberCheck: false, - PreventSecrets: false, - MaxFileSize: 42, - }), - ), - }, - // Update the project push rules to original config - { - SkipFunc: isRunningInCE, - Config: testAccGitlabProjectPushRulesConfig(rInt), - Check: resource.ComposeTestCheckFunc( - testAccCheckGitlabProjectPushRulesExists("gitlab_project_push_rules.foo", &pushRules), - testAccCheckGitlabProjectPushRulesAttributes(&pushRules, &testAccGitlabProjectPushRulesExpectedAttributes{ - CommitMessageRegex: "^(foo|bar).*", - BranchNameRegex: "^(foo|bar).*", - AuthorEmailRegex: "^(foo|bar).*", - FileNameRegex: "^(foo|bar).*", - DenyDeleteTag: true, - MemberCheck: true, - PreventSecrets: true, - MaxFileSize: 10, - }), - ), - }, - }, - }) -} - -func TestAccGitlabProjectPushRules_import(t *testing.T) { - rInt := acctest.RandInt() - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckGitlabProjectPushRulesDestroy, - Steps: []resource.TestStep{ - { - SkipFunc: isRunningInCE, - Config: testAccGitlabProjectPushRulesConfig(rInt), - }, - { - SkipFunc: isRunningInCE, - ResourceName: "gitlab_project_push_rules.foo", - ImportStateId: fmt.Sprintf("root/foo-%d", rInt), - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -func testAccCheckGitlabProjectPushRulesExists(n string, pushRules *gitlab.ProjectPushRules) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not Found: %s", n) - } - - repoName := rs.Primary.Attributes["project"] - if repoName == "" { - return fmt.Errorf("No project ID is set") - } - conn := testAccProvider.Meta().(*gitlab.Client) - gotPushRules, _, err := conn.Projects.GetProjectPushRules(repoName) - if err != nil { - return err - } - *pushRules = *gotPushRules - return nil - } -} - -type testAccGitlabProjectPushRulesExpectedAttributes struct { - CommitMessageRegex string - BranchNameRegex string - AuthorEmailRegex string - FileNameRegex string - DenyDeleteTag bool - MemberCheck bool - PreventSecrets bool - MaxFileSize int -} - -func testAccCheckGitlabProjectPushRulesAttributes(pushRules *gitlab.ProjectPushRules, want *testAccGitlabProjectPushRulesExpectedAttributes) resource.TestCheckFunc { - return func(s *terraform.State) error { - if pushRules.CommitMessageRegex != want.CommitMessageRegex { - return fmt.Errorf("got commit_message_regex %s; want %s", pushRules.CommitMessageRegex, want.CommitMessageRegex) - } - if pushRules.BranchNameRegex != want.BranchNameRegex { - return fmt.Errorf("got branch_name_regex %s; want %s", pushRules.BranchNameRegex, want.BranchNameRegex) - } - if pushRules.AuthorEmailRegex != want.AuthorEmailRegex { - return fmt.Errorf("got author_email_regex %s; want %s", pushRules.AuthorEmailRegex, want.AuthorEmailRegex) - } - if pushRules.FileNameRegex != want.FileNameRegex { - return fmt.Errorf("got file_name_regex %s; want %s", pushRules.FileNameRegex, want.FileNameRegex) - } - if pushRules.DenyDeleteTag != want.DenyDeleteTag { - return fmt.Errorf("got deny_delete_tag %t; want %t", pushRules.DenyDeleteTag, want.DenyDeleteTag) - } - if pushRules.MemberCheck != want.MemberCheck { - return fmt.Errorf("got member_check %t; want %t", pushRules.MemberCheck, want.MemberCheck) - } - if pushRules.PreventSecrets != want.PreventSecrets { - return fmt.Errorf("got prevent_secrets %t; want %t", pushRules.PreventSecrets, want.PreventSecrets) - } - if pushRules.MaxFileSize != want.MaxFileSize { - return fmt.Errorf("got max_file_size %d; want %d", pushRules.MaxFileSize, want.MaxFileSize) - } - return nil - } -} - -func testAccCheckGitlabProjectPushRulesDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*gitlab.Client) - - for _, rs := range s.RootModule().Resources { - if rs.Type != "gitlab_project" { - continue - } - - gotRepo, resp, err := conn.Projects.GetProject(rs.Primary.ID, nil) - if err == nil { - if gotRepo != nil && fmt.Sprintf("%d", gotRepo.ID) == rs.Primary.ID { - if gotRepo.MarkedForDeletionAt == nil { - return fmt.Errorf("Repository still exists") - } - } - } - if resp.StatusCode != 404 { - return err - } - return nil - } - return nil -} - -func testAccGitlabProjectPushRulesConfig(rInt int) string { - return fmt.Sprintf(` -resource "gitlab_project" "foo" { - name = "foo-%d" - description = "Terraform acceptance test - Push Rule" - visibility_level = "public" -} - -resource "gitlab_project_push_rules" "foo" { - project = "${gitlab_project.foo.id}" - commit_message_regex = "^(foo|bar).*" - branch_name_regex = "^(foo|bar).*" - author_email_regex = "^(foo|bar).*" - file_name_regex = "^(foo|bar).*" - deny_delete_tag = true - member_check = true - prevent_secrets = true - max_file_size = 10 -} -`, rInt) -} - -func testAccGitlabProjectPushRulesUpdate(rInt int) string { - return fmt.Sprintf(` -resource "gitlab_project" "foo" { - name = "foo-%d" - description = "Terraform acceptance test - Push Rule" - visibility_level = "public" -} - -resource "gitlab_project_push_rules" "foo" { - project = "${gitlab_project.foo.id}" - commit_message_regex = "^(fu|baz).*" - branch_name_regex = "^(fu|baz).*" - author_email_regex = "^(fu|baz).*" - file_name_regex = "^(fu|baz).*" - deny_delete_tag = false - member_check = false - prevent_secrets = false - max_file_size = 42 -} -`, rInt) -} diff --git a/gitlab/resource_gitlab_project_test.go b/gitlab/resource_gitlab_project_test.go index 3ac234c77..c2af7b310 100644 --- a/gitlab/resource_gitlab_project_test.go +++ b/gitlab/resource_gitlab_project_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" @@ -52,7 +53,7 @@ func TestAccGitlabProject_basic(t *testing.T) { Providers: testAccProviders, CheckDestroy: testAccCheckGitlabProjectDestroy, Steps: []resource.TestStep{ - // Step0 Create a project with all the features on (note: "archived" is "false") + // Create a project with all the features on (note: "archived" is "false") { Config: testAccGitlabProjectConfig(rInt), Check: resource.ComposeTestCheckFunc( @@ -60,7 +61,7 @@ func TestAccGitlabProject_basic(t *testing.T) { testAccCheckAggregateGitlabProject(&defaults, &received), ), }, - // Step1 Update the project to turn the features off (note: "archived" is "true") + // Update the project to turn the features off (note: "archived" is "true") { Config: testAccGitlabProjectUpdateConfig(rInt), Check: resource.ComposeTestCheckFunc( @@ -86,7 +87,7 @@ func TestAccGitlabProject_basic(t *testing.T) { }, &received), ), }, - // Step2 Update the project to turn the features on again (note: "archived" is "false") + // Update the project to turn the features on again (note: "archived" is "false") { Config: testAccGitlabProjectConfig(rInt), Check: resource.ComposeTestCheckFunc( @@ -94,7 +95,7 @@ func TestAccGitlabProject_basic(t *testing.T) { testAccCheckAggregateGitlabProject(&defaults, &received), ), }, - // Step3 Update the project creating the default branch + // Update the project creating the default branch { // Get the ID from the project data at the previous step SkipFunc: testAccGitlabProjectConfigDefaultBranchSkipFunc(&received, "master"), @@ -104,6 +105,108 @@ func TestAccGitlabProject_basic(t *testing.T) { testAccCheckAggregateGitlabProject(&defaultsMasterBranch, &received), ), }, + // Test import without push rules (checks read function) + { + ResourceName: "gitlab_project.foo", + ImportState: true, + ImportStateVerify: true, + }, + // Add all push rules to an existing project + { + SkipFunc: isRunningInCE, + Config: testAccGitlabProjectConfigPushRules(rInt, ` +author_email_regex = "foo_author" +branch_name_regex = "foo_branch" +commit_message_regex = "foo_commit" +commit_message_negative_regex = "foo_not_commit" +file_name_regex = "foo_file" +commit_committer_check = true +deny_delete_tag = true +member_check = true +prevent_secrets = true +reject_unsigned_commits = true +max_file_size = 123 +`), + Check: testAccCheckGitlabProjectPushRules("gitlab_project.foo", &gitlab.ProjectPushRules{ + AuthorEmailRegex: "foo_author", + BranchNameRegex: "foo_branch", + CommitMessageRegex: "foo_commit", + CommitMessageNegativeRegex: "foo_not_commit", + FileNameRegex: "foo_file", + CommitCommitterCheck: true, + DenyDeleteTag: true, + MemberCheck: true, + PreventSecrets: true, + RejectUnsignedCommits: true, + MaxFileSize: 123, + }), + }, + // Test import with a all push rules defined (checks read function) + { + SkipFunc: isRunningInCE, + ResourceName: "gitlab_project.foo", + ImportState: true, + ImportStateVerify: true, + }, + // Try to add push rules to an existing project in CE + { + SkipFunc: isRunningInEE, + Config: testAccGitlabProjectConfigPushRules(rInt, `author_email_regex = "foo_author"`), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("Project push rules are not supported in your version of GitLab")), + }, + // Update push rules + { + SkipFunc: isRunningInCE, + Config: testAccGitlabProjectConfigPushRules(rInt, `author_email_regex = "foo_author"`), + Check: testAccCheckGitlabProjectPushRules("gitlab_project.foo", &gitlab.ProjectPushRules{ + AuthorEmailRegex: "foo_author", + }), + }, + // Remove the push_rules block entirely. + // NOTE: The push rules will still exist upstream because the push_rules block is computed. + { + SkipFunc: isRunningInCE, + Config: testAccGitlabProjectConfigDefaultBranch(rInt, "master"), + Check: testAccCheckGitlabProjectPushRules("gitlab_project.foo", &gitlab.ProjectPushRules{ + AuthorEmailRegex: "foo_author", + }), + }, + // Add different push rules after the block was removed previously + { + SkipFunc: isRunningInCE, + Config: testAccGitlabProjectConfigPushRules(rInt, `branch_name_regex = "(feature|hotfix)\\/*"`), + Check: testAccCheckGitlabProjectPushRules("gitlab_project.foo", &gitlab.ProjectPushRules{ + BranchNameRegex: `(feature|hotfix)\/*`, + }), + }, + // Destroy the project so we can next test creating a project with push rules simultaneously + { + Config: testAccGitlabProjectConfigDefaultBranch(rInt, "master"), + Destroy: true, + Check: testAccCheckGitlabProjectDestroy, + }, + // Create a new project with push rules + { + SkipFunc: isRunningInCE, + Config: testAccGitlabProjectConfigPushRules(rInt, ` +author_email_regex = "foo_author" +max_file_size = 123 +`), + Check: testAccCheckGitlabProjectPushRules("gitlab_project.foo", &gitlab.ProjectPushRules{ + AuthorEmailRegex: "foo_author", + MaxFileSize: 123, + }), + }, + // Try to create a new project with all push rules in CE + { + SkipFunc: isRunningInEE, + Config: testAccGitlabProjectConfigPushRules(rInt, `author_email_regex = "foo_author"`), + ExpectError: regexp.MustCompile(regexp.QuoteMeta("Project push rules are not supported in your version of GitLab")), + }, + // Update to original project config + { + Config: testAccGitlabProjectConfig(rInt), + }, }, }) } @@ -413,6 +516,81 @@ func testAccCheckGitlabProjectInitializeWithReadme(project *gitlab.Project, want } } +func testAccCheckGitlabProjectPushRules(name string, wantPushRules *gitlab.ProjectPushRules) resource.TestCheckFunc { + return func(state *terraform.State) error { + client := testAccProvider.Meta().(*gitlab.Client) + projectResource := state.RootModule().Resources[name].Primary + + gotPushRules, _, err := client.Projects.GetProjectPushRules(projectResource.ID, nil) + if err != nil { + return err + } + + var messages []string + + if gotPushRules.AuthorEmailRegex != wantPushRules.AuthorEmailRegex { + messages = append(messages, fmt.Sprintf("author_email_regex (got: %q, wanted: %q)", + gotPushRules.AuthorEmailRegex, wantPushRules.AuthorEmailRegex)) + } + + if gotPushRules.BranchNameRegex != wantPushRules.BranchNameRegex { + messages = append(messages, fmt.Sprintf("branch_name_regex (got: %q, wanted: %q)", + gotPushRules.BranchNameRegex, wantPushRules.BranchNameRegex)) + } + + if gotPushRules.CommitMessageRegex != wantPushRules.CommitMessageRegex { + messages = append(messages, fmt.Sprintf("commit_message_regex (got: %q, wanted: %q)", + gotPushRules.CommitMessageRegex, wantPushRules.CommitMessageRegex)) + } + + if gotPushRules.CommitMessageNegativeRegex != wantPushRules.CommitMessageNegativeRegex { + messages = append(messages, fmt.Sprintf("commit_message_negative_regex (got: %q, wanted: %q)", + gotPushRules.CommitMessageNegativeRegex, wantPushRules.CommitMessageNegativeRegex)) + } + + if gotPushRules.FileNameRegex != wantPushRules.FileNameRegex { + messages = append(messages, fmt.Sprintf("file_name_regex (got: %q, wanted: %q)", + gotPushRules.FileNameRegex, wantPushRules.FileNameRegex)) + } + + if gotPushRules.CommitCommitterCheck != wantPushRules.CommitCommitterCheck { + messages = append(messages, fmt.Sprintf("commit_committer_check (got: %t, wanted: %t)", + gotPushRules.CommitCommitterCheck, wantPushRules.CommitCommitterCheck)) + } + + if gotPushRules.DenyDeleteTag != wantPushRules.DenyDeleteTag { + messages = append(messages, fmt.Sprintf("deny_delete_tag (got: %t, wanted: %t)", + gotPushRules.DenyDeleteTag, wantPushRules.DenyDeleteTag)) + } + + if gotPushRules.MemberCheck != wantPushRules.MemberCheck { + messages = append(messages, fmt.Sprintf("member_check (got: %t, wanted: %t)", + gotPushRules.MemberCheck, wantPushRules.MemberCheck)) + } + + if gotPushRules.PreventSecrets != wantPushRules.PreventSecrets { + messages = append(messages, fmt.Sprintf("prevent_secrets (got: %t, wanted: %t)", + gotPushRules.PreventSecrets, wantPushRules.PreventSecrets)) + } + + if gotPushRules.RejectUnsignedCommits != wantPushRules.RejectUnsignedCommits { + messages = append(messages, fmt.Sprintf("reject_unsigned_commits (got: %t, wanted: %t)", + gotPushRules.RejectUnsignedCommits, wantPushRules.RejectUnsignedCommits)) + } + + if gotPushRules.MaxFileSize != wantPushRules.MaxFileSize { + messages = append(messages, fmt.Sprintf("max_file_size (got: %d, wanted: %d)", + gotPushRules.MaxFileSize, wantPushRules.MaxFileSize)) + } + + if len(messages) > 0 { + return fmt.Errorf("unexpected push_rules:\n\t- %s", strings.Join(messages, "\n\t- ")) + } + + return nil + } +} + func testAccGitlabProjectInGroupConfig(rInt int) string { return fmt.Sprintf(` resource "gitlab_group" "foo" { @@ -569,3 +747,21 @@ resource "gitlab_project" "imported" { } `, rInt, importURL) } + +func testAccGitlabProjectConfigPushRules(rInt int, pushRules string) string { + return fmt.Sprintf(` +resource "gitlab_project" "foo" { + name = "foo-%[1]d" + path = "foo.%[1]d" + description = "Terraform acceptance tests" + default_branch = "master" + + push_rules { +%[2]s + } + + # So that acceptance tests can be run in a gitlab organization with no billing. + visibility_level = "public" +} + `, rInt, pushRules) +}