From a07bd291f53ae1c868fa2a03abe31b5ec471796a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Markert?= Date: Wed, 11 Sep 2024 17:32:48 +0000 Subject: [PATCH] feat: add branch_protection resource (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit added terraform tests for the resource Reviewed-on: https://gitea.com/gitea/terraform-provider-gitea/pulls/72 Co-authored-by: Jörg Markert Co-committed-by: Jörg Markert --- .gitea/workflows/test.yml | 4 +- .gitignore | 3 + .../resources/repository_branch_protection.md | 68 +++ examples/provider.tf | 4 +- .../gitea_repo_branch_protection/resource.tf | 14 + gitea/provider.go | 25 +- ...urce_gitea_repository_branch_protection.go | 495 ++++++++++++++++++ tests/branch_protection.tf | 29 + tests/branch_protection.tftest.hcl | 306 +++++++++++ tests/readme.md | 14 + tests/setup/main.tf | 22 + tests/setup/variables.tf | 15 + tests/setup/versions.tf | 16 + tests/variables.tf | 117 +++++ tests/versions.tf | 16 + 15 files changed, 1133 insertions(+), 15 deletions(-) create mode 100644 docs/resources/repository_branch_protection.md create mode 100644 examples/resources/gitea_repo_branch_protection/resource.tf create mode 100644 gitea/resource_gitea_repository_branch_protection.go create mode 100644 tests/branch_protection.tf create mode 100644 tests/branch_protection.tftest.hcl create mode 100644 tests/readme.md create mode 100644 tests/setup/main.tf create mode 100644 tests/setup/variables.tf create mode 100644 tests/setup/versions.tf create mode 100644 tests/variables.tf create mode 100644 tests/versions.tf diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 365507f..ca5899e 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -22,7 +22,9 @@ jobs: - name: Terraform Init id: init run: terraform init + working-directory: examples - name: Terraform Validate id: validate - run: terraform validate -no-color \ No newline at end of file + run: terraform validate -no-color + working-directory: examples diff --git a/.gitignore b/.gitignore index 19e0518..940f9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .vscode .idea/ dist/ +tests/terraform.tfvars +tests/.terraform +tests/.terraform.lock.hcl diff --git a/docs/resources/repository_branch_protection.md b/docs/resources/repository_branch_protection.md new file mode 100644 index 0000000..20db023 --- /dev/null +++ b/docs/resources/repository_branch_protection.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "gitea_repository_branch_protection Resource - terraform-provider-gitea" +subcategory: "" +description: |- + This resource allows you to create and manage branch protections for repositories. +--- + +# gitea_repository_branch_protection (Resource) + +This resource allows you to create and manage branch protections for repositories. + + + + +## Schema + +### Required + +- `name` (String) Repository name +- `rule_name` (String) Protected Branch Name Pattern +- `username` (String) User name or organization name + +### Optional + +- `approval_whitelist_teams` (List of String) Only reviews from allowlisted teams will count to the required + approvals. Without approval allowlist, reviews from anyone with + write access count to the required approvals. +- `approval_whitelist_users` (List of String) Only reviews from allowlisted users will count to the required + approvals. Without approval allowlist, reviews from anyone with + write access count to the required approvals. +- `block_merge_on_official_review_requests` (Boolean) Merging will not be possible when it has official + review requests, even if there are enough approvals. +- `block_merge_on_outdated_branch` (Boolean) Merging will not be possible when head branch is behind base branch. +- `block_merge_on_rejected_reviews` (Boolean) Merging will not be possible when changes are + requested by official reviewers, even if there are enough + approvals. +- `dismiss_stale_approvals` (Boolean) When new commits that change the content of the pull request + are pushed to the branch, old approvals will be dismissed. +- `enable_push` (Boolean) Anyone with write access will be allowed to push to this branch + (but not force push), add a whitelist users or teams to limit + access. +- `merge_whitelist_teams` (List of String) Allow only allowlisted teams to merge pull requests into this branch. +- `merge_whitelist_users` (List of String) Allow only allowlisted users to merge pull requests into this branch. +- `protected_file_patterns` (String) Protected file patterns (separated using semicolon ';') +- `push_whitelist_deploy_keys` (Boolean) Allow deploy keys with write access to push. Requires enable_push to be set to true. +- `push_whitelist_teams` (List of String) Allowlisted teams for pushing. Requires enable_push to be set to true. +- `push_whitelist_users` (List of String) Allowlisted users for pushing. Requires enable_push to be set to true. +- `require_signed_commits` (Boolean) Reject pushes to this branch if they are unsigned or unverifiable. +- `required_approvals` (Number) Allow only to merge pull request with enough positive reviews. +- `status_check_patterns` (List of String) Enter patterns to specify which status checks must pass before + branches can be merged into a branch that matches this rule. + Each line specifies a pattern. Patterns cannot be empty. +- `unprotected_file_patterns` (String) Unprotected file patterns (separated using semicolon ';') + +### Read-Only + +- `created_at` (String) Webhook creation timestamp +- `enable_approval_whitelist` (Boolean) True if a approval whitelist is used. +- `enable_merge_whitelist` (Boolean) True if a merge whitelist is used. +- `enable_push_whitelist` (Boolean) True if a push whitelist is used. +- `enable_status_check` (Boolean) Require status checks to pass before merging. When enabled, + commits must first be pushed to another branch, then merged + or pushed directly to a branch that matches this rule after + status checks have passed. If no contexts are matched, the + last commit must be successful regardless of context +- `id` (String) The ID of this resource. +- `updated_at` (String) Webhook creation timestamp diff --git a/examples/provider.tf b/examples/provider.tf index 8ac39ce..e05f192 100644 --- a/examples/provider.tf +++ b/examples/provider.tf @@ -2,7 +2,7 @@ terraform { required_providers { gitea = { source = "go-gitea/gitea" - version = "0.1.0" + version = "0.3.0" } } } @@ -12,4 +12,4 @@ provider "gitea" { username = "lerentis" password = var.gitea_password #token = var.gitea_token -} \ No newline at end of file +} diff --git a/examples/resources/gitea_repo_branch_protection/resource.tf b/examples/resources/gitea_repo_branch_protection/resource.tf new file mode 100644 index 0000000..119b108 --- /dev/null +++ b/examples/resources/gitea_repo_branch_protection/resource.tf @@ -0,0 +1,14 @@ +resource "gitea_repository" "repo" { + username = var.username + name = var.name + auto_init = false +} + +resource "gitea_repository_branch_protection" "main" { + username = gitea_repository.repo.username + name = gitea_repository.repo.name + + rule_name = "main" + enable_push = true + status_check_patterns = var.branch_protection_patterns +} diff --git a/gitea/provider.go b/gitea/provider.go index 808a14a..ea1c734 100644 --- a/gitea/provider.go +++ b/gitea/provider.go @@ -75,18 +75,19 @@ func Provider() *schema.Provider { "gitea_org": resourceGiteaOrg(), // "gitea_team": resourceGiteaTeam(), // "gitea_repo": resourceGiteaRepo(), - "gitea_user": resourceGiteaUser(), - "gitea_oauth2_app": resourceGiteaOauthApp(), - "gitea_repository": resourceGiteaRepository(), - "gitea_fork": resourceGiteaFork(), - "gitea_public_key": resourceGiteaPublicKey(), - "gitea_team": resourceGiteaTeam(), - "gitea_team_membership": resourceGiteaTeamMembership(), - "gitea_team_members": resourceGiteaTeamMembers(), - "gitea_git_hook": resourceGiteaGitHook(), - "gitea_token": resourceGiteaToken(), - "gitea_repository_key": resourceGiteaRepositoryKey(), - "gitea_repository_webhook": resourceGiteaRepositoryWebhook(), + "gitea_user": resourceGiteaUser(), + "gitea_oauth2_app": resourceGiteaOauthApp(), + "gitea_repository": resourceGiteaRepository(), + "gitea_fork": resourceGiteaFork(), + "gitea_public_key": resourceGiteaPublicKey(), + "gitea_team": resourceGiteaTeam(), + "gitea_team_membership": resourceGiteaTeamMembership(), + "gitea_team_members": resourceGiteaTeamMembers(), + "gitea_git_hook": resourceGiteaGitHook(), + "gitea_token": resourceGiteaToken(), + "gitea_repository_key": resourceGiteaRepositoryKey(), + "gitea_repository_webhook": resourceGiteaRepositoryWebhook(), + "gitea_repository_branch_protection": resourceGiteaRepositoryBranchProtection(), }, ConfigureFunc: providerConfigure, diff --git a/gitea/resource_gitea_repository_branch_protection.go b/gitea/resource_gitea_repository_branch_protection.go new file mode 100644 index 0000000..da5aae7 --- /dev/null +++ b/gitea/resource_gitea_repository_branch_protection.go @@ -0,0 +1,495 @@ +package gitea + +import ( + "log" + + "code.gitea.io/sdk/gitea" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + repoBPUsername string = "username" + repoBPName string = "name" + repoBPRuleName string = "rule_name" + + repoBPProtectedFilePatterns string = "protected_file_patterns" + repoBPUnprotectedFilePatterns string = "unprotected_file_patterns" + + repoBPEnablePush string = "enable_push" + repoBPEnablePushWhitelist string = "enable_push_whitelist" + repoBPPushWhitelistUsers string = "push_whitelist_users" + repoBPPushWhitelistTeams string = "push_whitelist_teams" + repoBPPushWhitelistDeployKeys string = "push_whitelist_deploy_keys" + + repoBPRequireSignedCommits string = "require_signed_commits" + + repoBPRequiredApprovals string = "required_approvals" + repoBPEnableApprovalWhitelist string = "enable_approval_whitelist" + repoBPApprovalWhitelistUsers string = "approval_whitelist_users" + repoBPApprovalWhitelistTeams string = "approval_whitelist_teams" + repoBPDismissStaleApprovals string = "dismiss_stale_approvals" + // not implemented in go-gitea-sdk + // repoBPIgnoreStaleApprovals string = "ignore_stale_approvals" + + repoBPEnableStatusCheck string = "enable_status_check" + repoBPStatusCheckPatterns string = "status_check_patterns" + + repoBPEnableMergeWhitelist string = "enable_merge_whitelist" + repoBPMergeWhitelistUsers string = "merge_whitelist_users" + repoBPMergeWhitelistTeams string = "merge_whitelist_teams" + + repoBPBlockMergeOnRejectedReviews string = "block_merge_on_rejected_reviews" + repoBPBlockMergeOnOfficialReviewRequests string = "block_merge_on_official_review_requests" + repoBPBlockMergeOnOutdatedBranch string = "block_merge_on_outdated_branch" + + repoBPUpdatedAt string = "updated_at" + repoBPCreatedAt string = "created_at" +) + +func resourceRepositoryBranchProtectionRead(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + user := d.Get(repoBPUsername).(string) + repo := d.Get(repoBPName).(string) + rule_name := d.Get(repoBPRuleName).(string) + + bp, resp, err := client.GetBranchProtection(user, repo, rule_name) + if err != nil { + if resp.StatusCode == 404 { + d.SetId("") + return + } else { + return err + } + } + + err = setRepositoryBranchProtectionData(bp, user, repo, d) + return err +} + +func generateWhitelist(d *schema.ResourceData, listname string) (enabled bool, users []string, teams []string) { + u := d.Get(listname + "_users") + users = make([]string, 0) + if u != nil { + for _, element := range u.([]interface{}) { + users = append(users, element.(string)) + } + } + + t := d.Get(listname + "_teams") + teams = make([]string, 0) + if u != nil { + for _, element := range t.([]interface{}) { + teams = append(teams, element.(string)) + } + } + + if c := len(users) + len(teams); c > 0 { + enabled = true + } + if listname == "push_whitelist" && d.Get(repoBPPushWhitelistDeployKeys).(bool) { + enabled = true + } + + log.Println("enabled?:", enabled, listname) + return enabled, users, teams +} + +func resourceRepositoryBranchProtectionCreate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + user := d.Get(repoBPUsername).(string) + repo := d.Get(repoBPName).(string) + + enablePushWhitelist, pushWhitelistUsernames, pushWhitelistTeams := generateWhitelist(d, "push_whitelist") + enableMergeWhitelist, mergeWhitelistUsernames, mergeWhitelistTeams := generateWhitelist(d, "merge_whitelist") + enableApprovalsWhitelist, approvalsWhitelistUsernames, approvalsWhitelistTeams := generateWhitelist(d, "approval_whitelist") + + statusCheckContexts := make([]string, 0) + for _, element := range d.Get(repoBPStatusCheckPatterns).([]interface{}) { + statusCheckContexts = append(statusCheckContexts, element.(string)) + } + + log.Println("create_ulist:", pushWhitelistUsernames) + + enableStatusCheck := false + if len(statusCheckContexts) > 0 { + enableStatusCheck = true + } + + bpOption := gitea.CreateBranchProtectionOption{ + // BranchName is deprecated in gitea, but still required in go-gitea-sdk, therefore using RuleName + BranchName: d.Get(repoBPRuleName).(string), + RuleName: d.Get(repoBPRuleName).(string), + EnablePush: d.Get(repoBPEnablePush).(bool), + EnablePushWhitelist: enablePushWhitelist, + PushWhitelistUsernames: pushWhitelistUsernames, + PushWhitelistTeams: pushWhitelistTeams, + PushWhitelistDeployKeys: d.Get(repoBPPushWhitelistDeployKeys).(bool), + EnableMergeWhitelist: enableMergeWhitelist, + MergeWhitelistUsernames: mergeWhitelistUsernames, + MergeWhitelistTeams: mergeWhitelistTeams, + EnableStatusCheck: enableStatusCheck, + StatusCheckContexts: statusCheckContexts, + RequiredApprovals: int64(d.Get(repoBPRequiredApprovals).(int)), + EnableApprovalsWhitelist: enableApprovalsWhitelist, + ApprovalsWhitelistUsernames: approvalsWhitelistUsernames, + ApprovalsWhitelistTeams: approvalsWhitelistTeams, + BlockOnRejectedReviews: d.Get(repoBPBlockMergeOnRejectedReviews).(bool), + BlockOnOfficialReviewRequests: d.Get(repoBPBlockMergeOnOfficialReviewRequests).(bool), + BlockOnOutdatedBranch: d.Get(repoBPBlockMergeOnOutdatedBranch).(bool), + DismissStaleApprovals: d.Get(repoBPDismissStaleApprovals).(bool), + // IgnoreStaleApprovals: d.Get(repoBPIgnoreStaleApprovals).(bool), + RequireSignedCommits: d.Get(repoBPRequireSignedCommits).(bool), + ProtectedFilePatterns: d.Get(repoBPProtectedFilePatterns).(string), + UnprotectedFilePatterns: d.Get(repoBPUnprotectedFilePatterns).(string), + } + + bp, _, err := client.CreateBranchProtection(user, repo, bpOption) + if err != nil { + return err + } + + err = setRepositoryBranchProtectionData(bp, user, repo, d) + return err +} + +func resourceRepositoryBranchProtectionUpdate(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + user := d.Get(repoBPUsername).(string) + repo := d.Get(repoBPName).(string) + rule_name := d.Id() + + enablePushWhitelist, pushWhitelistUsernames, pushWhitelistTeams := generateWhitelist(d, "push_whitelist") + enableMergeWhitelist, mergeWhitelistUsernames, mergeWhitelistTeams := generateWhitelist(d, "merge_whitelist") + enableApprovalsWhitelist, approvalsWhitelistUsernames, approvalsWhitelistTeams := generateWhitelist(d, "approval_whitelist") + + statusCheckContexts := make([]string, 0) + for _, element := range d.Get(repoBPStatusCheckPatterns).([]interface{}) { + statusCheckContexts = append(statusCheckContexts, element.(string)) + } + + enablePush := false + if enablePushWhitelist == true || d.Get(repoBPEnablePush).(bool) == true { + enablePush = true + } + pushWhitelistDeployKeys := d.Get(repoBPPushWhitelistDeployKeys).(bool) + enableStatusCheck := false + if len(statusCheckContexts) > 0 { + enableStatusCheck = true + } + requiredApprovals := int64(d.Get(repoBPRequiredApprovals).(int)) + blockOnRejectedReviews := d.Get(repoBPBlockMergeOnRejectedReviews).(bool) + blockOnOfficialReviewRequests := d.Get(repoBPBlockMergeOnOfficialReviewRequests).(bool) + blockOnOutdatedBranch := d.Get(repoBPBlockMergeOnOutdatedBranch).(bool) + dismissStaleApprovals := d.Get(repoBPDismissStaleApprovals).(bool) + // ignoreStaleApprovals := d.Get(repoBPIgnoreStaleApprovals).(bool) + requireSignedCommits := d.Get(repoBPRequireSignedCommits).(bool) + protectedFilePatterns := d.Get(repoBPProtectedFilePatterns).(string) + unprotectedFilePatterns := d.Get(repoBPUnprotectedFilePatterns).(string) + + bpOption := gitea.EditBranchProtectionOption{ + EnablePush: &enablePush, + EnablePushWhitelist: &enablePushWhitelist, + PushWhitelistUsernames: pushWhitelistUsernames, + PushWhitelistTeams: pushWhitelistTeams, + PushWhitelistDeployKeys: &pushWhitelistDeployKeys, + EnableMergeWhitelist: &enableMergeWhitelist, + MergeWhitelistUsernames: mergeWhitelistUsernames, + MergeWhitelistTeams: mergeWhitelistTeams, + EnableStatusCheck: &enableStatusCheck, + StatusCheckContexts: statusCheckContexts, + RequiredApprovals: &requiredApprovals, + EnableApprovalsWhitelist: &enableApprovalsWhitelist, + ApprovalsWhitelistUsernames: approvalsWhitelistUsernames, + ApprovalsWhitelistTeams: approvalsWhitelistTeams, + BlockOnRejectedReviews: &blockOnRejectedReviews, + BlockOnOfficialReviewRequests: &blockOnOfficialReviewRequests, + BlockOnOutdatedBranch: &blockOnOutdatedBranch, + DismissStaleApprovals: &dismissStaleApprovals, + // IgnoreStaleApprovals: &ignoreStaleApprovals, + RequireSignedCommits: &requireSignedCommits, + ProtectedFilePatterns: &protectedFilePatterns, + UnprotectedFilePatterns: &unprotectedFilePatterns, + } + + bp, _, err := client.EditBranchProtection(user, repo, rule_name, bpOption) + if err != nil { + return err + } + + err = setRepositoryBranchProtectionData(bp, user, repo, d) + return err +} + +func resourceRepositoryBranchProtectionDelete(d *schema.ResourceData, meta interface{}) (err error) { + client := meta.(*gitea.Client) + + user := d.Get(repoBPUsername).(string) + repo := d.Get(repoBPName).(string) + rule_name := d.Id() + + _, err = client.DeleteBranchProtection(user, repo, rule_name) + if err != nil { + return err + } + return err +} + +func setRepositoryBranchProtectionData(bp *gitea.BranchProtection, user string, repo string, d *schema.ResourceData) (err error) { + d.SetId(bp.RuleName) + d.Set(repoBPUsername, user) + d.Set(repoBPName, repo) + d.Set(repoBPProtectedFilePatterns, bp.ProtectedFilePatterns) + d.Set(repoBPUnprotectedFilePatterns, bp.UnprotectedFilePatterns) + d.Set(repoBPEnablePush, bp.EnablePush) + d.Set(repoBPEnablePushWhitelist, bp.EnablePushWhitelist) + d.Set(repoBPPushWhitelistUsers, bp.PushWhitelistUsernames) + d.Set(repoBPPushWhitelistTeams, bp.PushWhitelistTeams) + d.Set(repoBPPushWhitelistDeployKeys, bp.PushWhitelistDeployKeys) + d.Set(repoBPRequireSignedCommits, bp.RequireSignedCommits) + d.Set(repoBPRequiredApprovals, bp.RequiredApprovals) + d.Set(repoBPEnableApprovalWhitelist, bp.EnableApprovalsWhitelist) + d.Set(repoBPApprovalWhitelistUsers, bp.ApprovalsWhitelistUsernames) + d.Set(repoBPApprovalWhitelistTeams, bp.ApprovalsWhitelistTeams) + d.Set(repoBPDismissStaleApprovals, bp.DismissStaleApprovals) + // d.Set(repoBPIgnoreStaleApprovals, bp.IgnoreStaleApprovals) + d.Set(repoBPEnableStatusCheck, bp.EnableStatusCheck) + d.Set(repoBPStatusCheckPatterns, bp.StatusCheckContexts) + d.Set(repoBPEnableMergeWhitelist, bp.EnableMergeWhitelist) + d.Set(repoBPMergeWhitelistUsers, bp.MergeWhitelistUsernames) + d.Set(repoBPMergeWhitelistTeams, bp.MergeWhitelistTeams) + d.Set(repoBPBlockMergeOnRejectedReviews, bp.BlockOnRejectedReviews) + d.Set(repoBPBlockMergeOnOfficialReviewRequests, bp.BlockOnOfficialReviewRequests) + d.Set(repoBPBlockMergeOnOutdatedBranch, bp.BlockOnOutdatedBranch) + d.Set(repoBPUpdatedAt, bp.Updated) + d.Set(repoBPCreatedAt, bp.Created) + + return err +} + +func resourceGiteaRepositoryBranchProtection() *schema.Resource { + return &schema.Resource{ + Read: resourceRepositoryBranchProtectionRead, + Create: resourceRepositoryBranchProtectionCreate, + Update: resourceRepositoryBranchProtectionUpdate, + Delete: resourceRepositoryBranchProtectionDelete, + // TODO: importer ? + Schema: map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "User name or organization name", + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Repository name", + }, + "rule_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Protected Branch Name Pattern", + }, + "protected_file_patterns": { + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Default: "", + Description: "Protected file patterns (separated using semicolon ';')", + }, + "unprotected_file_patterns": { + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Default: "", + Description: "Unprotected file patterns (separated using semicolon ';')", + }, + "enable_push": { + Type: schema.TypeBool, + Optional: true, + ForceNew: false, + Default: false, + Description: `Anyone with write access will be allowed to push to this branch + (but not force push), add a whitelist users or teams to limit + access.`, + }, + "enable_push_whitelist": { + Type: schema.TypeBool, + Computed: true, + Description: "True if a push whitelist is used.", + }, + "push_whitelist_users": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + RequiredWith: []string{"enable_push"}, + Optional: true, + ForceNew: false, + Description: "Allowlisted users for pushing. Requires enable_push to be set to true.", + }, + "push_whitelist_teams": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + RequiredWith: []string{"enable_push"}, + Optional: true, + ForceNew: false, + Description: "Allowlisted teams for pushing. Requires enable_push to be set to true.", + }, + "push_whitelist_deploy_keys": { + Type: schema.TypeBool, + RequiredWith: []string{"enable_push"}, + Optional: true, + ForceNew: false, + Default: false, + Description: "Allow deploy keys with write access to push. Requires enable_push to be set to true.", + }, + "require_signed_commits": { + Type: schema.TypeBool, + Optional: true, + ForceNew: false, + Default: false, + Description: "Reject pushes to this branch if they are unsigned or unverifiable.", + }, + "required_approvals": { + Type: schema.TypeInt, + Optional: true, + ForceNew: false, + Default: 0, + Description: "Allow only to merge pull request with enough positive reviews.", + }, + "enable_approval_whitelist": { + Type: schema.TypeBool, + Computed: true, + Description: "True if a approval whitelist is used.", + }, + "approval_whitelist_users": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: false, + Description: `Only reviews from allowlisted users will count to the required + approvals. Without approval allowlist, reviews from anyone with + write access count to the required approvals.`, + }, + "approval_whitelist_teams": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: false, + Description: `Only reviews from allowlisted teams will count to the required + approvals. Without approval allowlist, reviews from anyone with + write access count to the required approvals.`, + }, + "dismiss_stale_approvals": { + Type: schema.TypeBool, + Optional: true, + ForceNew: false, + Default: false, + Description: `When new commits that change the content of the pull request + are pushed to the branch, old approvals will be dismissed.`, + }, + // + // not implemented in go-gitea-sdk + // + // "ignore_stale_approvals": { + // Type: schema.TypeBool, + // Optional true, + // ForceNew: false, + // Default: false, + // Description: `Do not count approvals that were made on older commits (stale + // reviews) towards how many approvals the PR has. Irrelevant if + // stale reviews are already dismissed.`, + // }, + "enable_status_check": { + Type: schema.TypeBool, + Computed: true, + Description: `Require status checks to pass before merging. When enabled, + commits must first be pushed to another branch, then merged + or pushed directly to a branch that matches this rule after + status checks have passed. If no contexts are matched, the + last commit must be successful regardless of context`, + }, + "status_check_patterns": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: false, + Description: `Enter patterns to specify which status checks must pass before + branches can be merged into a branch that matches this rule. + Each line specifies a pattern. Patterns cannot be empty.`, + }, + "enable_merge_whitelist": { + Type: schema.TypeBool, + Computed: true, + Description: "True if a merge whitelist is used.", + }, + "merge_whitelist_users": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: false, + Description: "Allow only allowlisted users to merge pull requests into this branch.", + }, + "merge_whitelist_teams": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + ForceNew: false, + Description: "Allow only allowlisted teams to merge pull requests into this branch.", + }, + "block_merge_on_rejected_reviews": { + Type: schema.TypeBool, + Optional: true, + ForceNew: false, + Default: false, + Description: `Merging will not be possible when changes are + requested by official reviewers, even if there are enough + approvals.`, + }, + "block_merge_on_official_review_requests": { + Type: schema.TypeBool, + Optional: true, + ForceNew: false, + Default: false, + Description: `Merging will not be possible when it has official + review requests, even if there are enough approvals.`, + }, + "block_merge_on_outdated_branch": { + Type: schema.TypeBool, + Optional: true, + ForceNew: false, + Default: false, + Description: "Merging will not be possible when head branch is behind base branch.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Webhook creation timestamp", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Webhook creation timestamp", + }, + }, + Description: "This resource allows you to create and manage branch protections for repositories.", + } +} diff --git a/tests/branch_protection.tf b/tests/branch_protection.tf new file mode 100644 index 0000000..cfe5edc --- /dev/null +++ b/tests/branch_protection.tf @@ -0,0 +1,29 @@ +resource "gitea_repository_branch_protection" "bp" { + username = var.repo_user_name + name = var.repo_name + rule_name = var.rule_name + protected_file_patterns = var.protected_file_patterns + unprotected_file_patterns = var.unprotected_file_patterns + enable_push = var.enable_push + push_whitelist_users = var.push_whitelist_users + push_whitelist_teams = var.push_whitelist_teams + push_whitelist_deploy_keys = var.push_whitelist_deploy_keys + require_signed_commits = var.require_signed_commits + required_approvals = var.required_approvals + approval_whitelist_users = var.approval_whitelist_users + approval_whitelist_teams = var.approval_whitelist_teams + dismiss_stale_approvals = var.dismiss_stale_approvals + status_check_patterns = var.status_check_patterns + merge_whitelist_users = var.merge_whitelist_users + merge_whitelist_teams = var.merge_whitelist_teams + block_merge_on_rejected_reviews = var.block_merge_on_rejected_reviews + block_merge_on_official_review_requests = var.block_merge_on_official_review_requests + block_merge_on_outdated_branch = var.block_merge_on_outdated_branch +} + +# // +# // not implemented in go-gitea-sdk +# // +# // "ignore_stale_approvals": { +# // Description: `Do not count approvals that were made on older commits (stale reviews) towards how many approvals the PR has. Irrelevant if stale reviews are already dismissed.`, +# // }, diff --git a/tests/branch_protection.tftest.hcl b/tests/branch_protection.tftest.hcl new file mode 100644 index 0000000..666efd7 --- /dev/null +++ b/tests/branch_protection.tftest.hcl @@ -0,0 +1,306 @@ +# run "create_org" { +# assert { +# condition = gitea_org.test_org.name == var.org_name +# error_message = "${gitea_org.test_org.name} not eq ${var.org_name}" +# } +# } +# +# run "create_repo" { +# assert { +# condition = gitea_repository.org_repo.name == var.repo_name +# error_message = "${gitea_repository.org_repo.name} not eq ${var.repo_name}" +# } +# } + +# run "create_user" { +# assert { +# condition = gitea_user.test_user.username == var.user_name +# error_message = "${gitea_user.test_user.username} not eq ${var.user_name}" +# } +# } +# +# run "create_user_repo" { +# assert { +# condition = gitea_repository.user_repo.name == var.repo_name +# error_message = "${gitea_repository.user_repo.name} not eq ${var.repo_name}" +# } +# } + +run "setup" { + module { + source = "./setup" + } +} + +run "apply_defaults" { + variables { + rule_name = "apply_defaults" + } + assert { + condition = gitea_repository_branch_protection.bp.name == var.repo_name + error_message = "${gitea_repository_branch_protection.bp.name} not eq ${var.repo_name}" + } + assert { + condition = gitea_repository_branch_protection.bp.username == var.user_name + error_message = "${gitea_repository_branch_protection.bp.username} not eq ${var.user_name}" + } + assert { + condition = gitea_repository_branch_protection.bp.rule_name == var.rule_name + error_message = "${gitea_repository_branch_protection.bp.rule_name} not eq ${var.rule_name}" + } + assert { + condition = gitea_repository_branch_protection.bp.protected_file_patterns == var.protected_file_patterns + error_message = "${gitea_repository_branch_protection.bp.protected_file_patterns} not eq ${var.protected_file_patterns}" + } + assert { + condition = gitea_repository_branch_protection.bp.unprotected_file_patterns == var.unprotected_file_patterns + error_message = "${gitea_repository_branch_protection.bp.unprotected_file_patterns} not eq ${var.unprotected_file_patterns}" + } + assert { + condition = gitea_repository_branch_protection.bp.enable_push == var.enable_push + error_message = "${gitea_repository_branch_protection.bp.enable_push} not eq ${var.enable_push}" + } + assert { + condition = gitea_repository_branch_protection.bp.push_whitelist_users == var.push_whitelist_users + error_message = "${join(",", gitea_repository_branch_protection.bp.push_whitelist_users)} not eq ${join(",", var.push_whitelist_users)}" + } + assert { + condition = gitea_repository_branch_protection.bp.push_whitelist_teams == var.push_whitelist_teams + error_message = "${join(",", gitea_repository_branch_protection.bp.push_whitelist_teams)} not eq ${join(",", var.push_whitelist_teams)}" + } + assert { + condition = gitea_repository_branch_protection.bp.push_whitelist_deploy_keys == var.push_whitelist_deploy_keys + error_message = "${gitea_repository_branch_protection.bp.push_whitelist_deploy_keys} not eq ${var.push_whitelist_deploy_keys}" + } + assert { + condition = gitea_repository_branch_protection.bp.require_signed_commits == var.require_signed_commits + error_message = "${gitea_repository_branch_protection.bp.require_signed_commits} not eq ${var.require_signed_commits}" + } + assert { + condition = gitea_repository_branch_protection.bp.required_approvals == var.required_approvals + error_message = "${gitea_repository_branch_protection.bp.required_approvals} not eq ${var.required_approvals}" + } + assert { + condition = gitea_repository_branch_protection.bp.approval_whitelist_users == var.approval_whitelist_users + error_message = "${join(",", gitea_repository_branch_protection.bp.approval_whitelist_users)} not eq ${join(",", var.approval_whitelist_users)}" + } + assert { + condition = gitea_repository_branch_protection.bp.approval_whitelist_teams == var.approval_whitelist_teams + error_message = "${join(",", gitea_repository_branch_protection.bp.approval_whitelist_teams)} not eq ${join(",", var.approval_whitelist_teams)}" + } + assert { + condition = gitea_repository_branch_protection.bp.dismiss_stale_approvals == var.dismiss_stale_approvals + error_message = "${gitea_repository_branch_protection.bp.dismiss_stale_approvals} not eq ${var.dismiss_stale_approvals}" + } + assert { + condition = gitea_repository_branch_protection.bp.status_check_patterns == var.status_check_patterns + error_message = "${join(",", gitea_repository_branch_protection.bp.status_check_patterns)} not eq ${join(",", var.status_check_patterns)}" + } + assert { + condition = gitea_repository_branch_protection.bp.merge_whitelist_users == var.merge_whitelist_users + error_message = "${join(",", gitea_repository_branch_protection.bp.merge_whitelist_users)} not eq ${join(",", var.merge_whitelist_users)}" + } + assert { + condition = gitea_repository_branch_protection.bp.merge_whitelist_teams == var.merge_whitelist_teams + error_message = "${join(",", gitea_repository_branch_protection.bp.merge_whitelist_teams)} not eq ${join(",", var.merge_whitelist_teams)}" + } + assert { + condition = gitea_repository_branch_protection.bp.block_merge_on_rejected_reviews == var.block_merge_on_rejected_reviews + error_message = "${gitea_repository_branch_protection.bp.block_merge_on_rejected_reviews} not eq ${var.block_merge_on_rejected_reviews}" + } + assert { + condition = gitea_repository_branch_protection.bp.block_merge_on_official_review_requests == var.block_merge_on_official_review_requests + error_message = "${gitea_repository_branch_protection.bp.block_merge_on_official_review_requests} not eq ${var.block_merge_on_official_review_requests}" + } + assert { + condition = gitea_repository_branch_protection.bp.block_merge_on_outdated_branch == var.block_merge_on_outdated_branch + error_message = "${gitea_repository_branch_protection.bp.block_merge_on_outdated_branch} not eq ${var.block_merge_on_outdated_branch}" + } +} + +run "simple_params" { + variables { + rule_name = "simple_params" + protected_file_patterns = "foobar.yaml" + unprotected_file_patterns = "foobar.yaml" + require_signed_commits = true + required_approvals = 10 + dismiss_stale_approvals = true + block_merge_on_rejected_reviews = true + block_merge_on_official_review_requests = true + block_merge_on_outdated_branch = true + } + assert { + condition = gitea_repository_branch_protection.bp.protected_file_patterns == var.protected_file_patterns + error_message = "${gitea_repository_branch_protection.bp.protected_file_patterns} not eq ${var.protected_file_patterns}" + } + assert { + condition = gitea_repository_branch_protection.bp.unprotected_file_patterns == var.unprotected_file_patterns + error_message = "${gitea_repository_branch_protection.bp.unprotected_file_patterns} not eq ${var.unprotected_file_patterns}" + } + assert { + condition = gitea_repository_branch_protection.bp.require_signed_commits == var.require_signed_commits + error_message = "${gitea_repository_branch_protection.bp.require_signed_commits} not eq ${var.require_signed_commits}" + } + assert { + condition = gitea_repository_branch_protection.bp.required_approvals == var.required_approvals + error_message = "${gitea_repository_branch_protection.bp.required_approvals} not eq ${var.required_approvals}" + } + assert { + condition = gitea_repository_branch_protection.bp.dismiss_stale_approvals == var.dismiss_stale_approvals + error_message = "${gitea_repository_branch_protection.bp.dismiss_stale_approvals} not eq ${var.dismiss_stale_approvals}" + } + assert { + condition = gitea_repository_branch_protection.bp.block_merge_on_rejected_reviews == var.block_merge_on_rejected_reviews + error_message = "${gitea_repository_branch_protection.bp.block_merge_on_rejected_reviews} not eq ${var.block_merge_on_rejected_reviews}" + } + assert { + condition = gitea_repository_branch_protection.bp.block_merge_on_official_review_requests == var.block_merge_on_official_review_requests + error_message = "${gitea_repository_branch_protection.bp.block_merge_on_official_review_requests} not eq ${var.block_merge_on_official_review_requests}" + } + assert { + condition = gitea_repository_branch_protection.bp.block_merge_on_outdated_branch == var.block_merge_on_outdated_branch + error_message = "${gitea_repository_branch_protection.bp.block_merge_on_outdated_branch} not eq ${var.block_merge_on_outdated_branch}" + } +} + +run "enable_push" { + variables { + rule_name = "enable_push" + enable_push = true + } + assert { + condition = gitea_repository_branch_protection.bp.enable_push == var.enable_push + error_message = "${gitea_repository_branch_protection.bp.enable_push} not eq ${var.enable_push}" + } + assert { + condition = gitea_repository_branch_protection.bp.enable_push_whitelist == false + error_message = "${gitea_repository_branch_protection.bp.enable_push_whitelist} not eq `false`" + } +} + +run "implicit_push_whitelist_with_users" { + variables { + enable_push = true + rule_name = "implicit_and_push_whitelist_with_users" + push_whitelist_users = ["test_user"] + } + assert { + condition = gitea_repository_branch_protection.bp.enable_push_whitelist == true + error_message = "${gitea_repository_branch_protection.bp.enable_push_whitelist} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.push_whitelist_users == tolist(var.push_whitelist_users) + error_message = "${join(",", gitea_repository_branch_protection.bp.push_whitelist_users)} not eq ${join(",", var.push_whitelist_users)}" + } +} + +run "implicit_push_whitelist_with_teams" { + variables { + rule_name = "implicit_and_push_whitelist_with_teams" + repo_user_name = "test-org" + enable_push = true + push_whitelist_teams = ["Owners"] + } + assert { + condition = gitea_repository_branch_protection.bp.enable_push_whitelist == true + error_message = "${gitea_repository_branch_protection.bp.enable_push_whitelist} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.push_whitelist_teams == tolist(var.push_whitelist_teams) + error_message = "${join(",", gitea_repository_branch_protection.bp.push_whitelist_teams)} not eq ${join(",", var.push_whitelist_teams)}" + } +} + +run "implicit_push_whitelist_with_deploy_keys" { + variables { + rule_name = "implicit_push_whitelist_with_deploy_keys" + enable_push = true + push_whitelist_deploy_keys = true + } + assert { + condition = gitea_repository_branch_protection.bp.enable_push_whitelist == true + error_message = "${gitea_repository_branch_protection.bp.enable_push_whitelist} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.push_whitelist_deploy_keys == var.push_whitelist_deploy_keys + error_message = "${gitea_repository_branch_protection.bp.push_whitelist_deploy_keys} not eq ${var.push_whitelist_deploy_keys}" + } +} + +run "implicit_enable_approve_whitelist_with_users" { + variables { + rule_name = "implicit_enable_approve_whitelist_with_users" + approval_whitelist_users = ["test_user"] + } + assert { + condition = gitea_repository_branch_protection.bp.enable_approval_whitelist == true + error_message = "${gitea_repository_branch_protection.bp.enable_approval_whitelist} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.approval_whitelist_users == tolist(var.approval_whitelist_users) + error_message = "${join(",", gitea_repository_branch_protection.bp.approval_whitelist_users)} not eq ${join(",", var.approval_whitelist_users)}" + } +} + +run "implicit_enable_approve_whitelist_with_teams" { + variables { + rule_name = "implicit_enable_approve_whitelist_with_teams" + repo_user_name = "test-org" + approval_whitelist_teams = ["Owners"] + } + assert { + condition = gitea_repository_branch_protection.bp.enable_approval_whitelist == true + error_message = "${gitea_repository_branch_protection.bp.enable_approval_whitelist} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.approval_whitelist_teams == tolist(var.approval_whitelist_teams) + error_message = "${join(",", gitea_repository_branch_protection.bp.approval_whitelist_teams)} not eq ${join(",", var.approval_whitelist_teams)}" + } +} + +run "implicit_enable_merge_whitelist_with_users" { + variables { + rule_name = "implicit_enable_merge_whitelist_with_users" + merge_whitelist_users = ["test_user"] + } + assert { + condition = gitea_repository_branch_protection.bp.enable_merge_whitelist == true + error_message = "${gitea_repository_branch_protection.bp.enable_merge_whitelist} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.merge_whitelist_users == tolist(var.merge_whitelist_users) + error_message = "${join(",", gitea_repository_branch_protection.bp.merge_whitelist_users)} not eq ${join(",", var.merge_whitelist_users)}" + } +} + +run "implicit_enable_merge_whitelist_with_teams" { + variables { + rule_name = "implicit_enable_merge_whitelist_with_teams" + repo_user_name = "test-org" + merge_whitelist_teams = ["Owners"] + } + assert { + condition = gitea_repository_branch_protection.bp.enable_merge_whitelist == true + error_message = "${gitea_repository_branch_protection.bp.enable_merge_whitelist} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.merge_whitelist_teams == tolist(var.merge_whitelist_teams) + error_message = "${join(",", gitea_repository_branch_protection.bp.merge_whitelist_teams)} not eq ${join(",", var.merge_whitelist_teams)}" + } +} + +run "implicit_enable_status_check" { + variables { + rule_name = "implicit_enable_status_check" + status_check_patterns = ["terraform-tests", "tf fmt"] + } + assert { + condition = gitea_repository_branch_protection.bp.enable_status_check == true + error_message = "${gitea_repository_branch_protection.bp.enable_status_check} not eq `true`" + } + assert { + condition = gitea_repository_branch_protection.bp.status_check_patterns == tolist(var.status_check_patterns) + error_message = "${join(",", gitea_repository_branch_protection.bp.status_check_patterns)} not eq ${join(",", var.status_check_patterns)}" + } +} diff --git a/tests/readme.md b/tests/readme.md new file mode 100644 index 0000000..6845543 --- /dev/null +++ b/tests/readme.md @@ -0,0 +1,14 @@ +# How to run tests + +```bash +docker run -p 3000:3000 gitea/gitea:latest-rootless +``` + +1. Setup gitea under http://localhost:3000 +2. Username: gitea_admin +3. Password: gitea_admin +4. Email: admin@gitea.local + +```bash +terraform test +``` diff --git a/tests/setup/main.tf b/tests/setup/main.tf new file mode 100644 index 0000000..24ce609 --- /dev/null +++ b/tests/setup/main.tf @@ -0,0 +1,22 @@ +resource "gitea_org" "test_org" { + name = var.org_name +} + +resource "gitea_repository" "org_repo" { + username = gitea_org.test_org.name + name = var.repo_name +} + +resource "gitea_user" "test_user" { + password = "Geheim1!" + email = "terraform@local.host" + username = var.user_name + login_name = var.user_name + must_change_password = false +} + +resource "gitea_repository" "user_repo" { + username = gitea_user.test_user.username + name = var.repo_name +} + diff --git a/tests/setup/variables.tf b/tests/setup/variables.tf new file mode 100644 index 0000000..e12d57c --- /dev/null +++ b/tests/setup/variables.tf @@ -0,0 +1,15 @@ +variable "repo_name" { + type = string + default = "test-repo" +} + +variable "org_name" { + type = string + default = "test-org" +} + +variable "user_name" { + type = string + default = "test_user" +} + diff --git a/tests/setup/versions.tf b/tests/setup/versions.tf new file mode 100644 index 0000000..e4cf5a1 --- /dev/null +++ b/tests/setup/versions.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + gitea = { + source = "go-gitea/gitea" + version = "0.3.0" + } + } + required_version = ">= 0.13" +} + +provider "gitea" { + base_url = "http://localhost:3000" + username = "gitea_admin" + password = "gitea_admin" + insecure = true +} diff --git a/tests/variables.tf b/tests/variables.tf new file mode 100644 index 0000000..ebd84d7 --- /dev/null +++ b/tests/variables.tf @@ -0,0 +1,117 @@ +variable "repo_name" { + type = string + default = "test-repo" +} + +variable "org_name" { + type = string + default = "test-org" +} + +variable "repo_user_name" { + type = string + default = "test_user" +} + +variable "user_name" { + type = string + default = "test_user" +} + +variable "rule_name" { + type = string + default = "branch-protection" +} + +variable "protected_file_patterns" { + type = string + default = "" +} + +variable "unprotected_file_patterns" { + type = string + default = "" +} + +variable "enable_push" { + type = bool + default = true +} + +variable "push_whitelist_users" { + type = list(string) + default = [] +} + +variable "push_whitelist_teams" { + type = list(string) + default = [] +} + +variable "push_whitelist_deploy_keys" { + type = bool + default = false +} + +variable "require_signed_commits" { + type = bool + default = false +} + +variable "required_approvals" { + type = number + default = 0 +} + +variable "approval_whitelist_users" { + type = list(string) + default = [] +} + +variable "approval_whitelist_teams" { + type = list(string) + default = [] +} + +variable "dismiss_stale_approvals" { + type = bool + default = false +} + +variable "status_check_patterns" { + type = list(string) + default = [] +} + +variable "merge_whitelist_users" { + type = list(string) + default = [] +} + +variable "merge_whitelist_teams" { + type = list(string) + default = [] +} + +variable "block_merge_on_rejected_reviews" { + type = bool + default = false +} + +variable "block_merge_on_official_review_requests" { + type = bool + default = false +} + +variable "block_merge_on_outdated_branch" { + type = bool + default = false +} + +# // not implemented in go-gitea-sdk +// +# // +# // "ignore_stale_approvals": { +# // Description: `Do not count approvals that were made on older commits (stale reviews) towards how many approvals the PR has. Irrelevant if stale reviews are already dismissed.`, +# // }, + diff --git a/tests/versions.tf b/tests/versions.tf new file mode 100644 index 0000000..e4cf5a1 --- /dev/null +++ b/tests/versions.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + gitea = { + source = "go-gitea/gitea" + version = "0.3.0" + } + } + required_version = ">= 0.13" +} + +provider "gitea" { + base_url = "http://localhost:3000" + username = "gitea_admin" + password = "gitea_admin" + insecure = true +}