Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding new resource azuredevops_repository_policy_searchable_branches #1272

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package acceptancetests

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/acceptancetests/testutils"
)

func TestAccRepositoryPolicySearchableBranches(t *testing.T) {
testutils.RunTestsInSequence(t, map[string]map[string]func(t *testing.T){
"RepositoryPolicies": {
"basic": testAccRepositoryPolicySearchableBranchesBasic,
"update": testAccRepositoryPolicySearchableBranchesUpdate,
},
})
}

func testAccRepositoryPolicySearchableBranchesBasic(t *testing.T) {
searchableBranchesTfNode := "azuredevops_repository_searchable_branches.test"
projectName := testutils.GenerateResourceName()
repoName := testutils.GenerateResourceName()

resource.Test(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
Steps: []resource.TestStep{
{
Config: hclRepoPolicySearchableBranchesResourceBasic(projectName, repoName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(searchableBranchesTfNode, "searchable_branches.#", "1"),
),
}, {
ResourceName: searchableBranchesTfNode,
ImportStateIdFunc: testutils.ComputeProjectQualifiedResourceImportID(searchableBranchesTfNode),
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func testAccRepositoryPolicySearchableBranchesUpdate(t *testing.T) {
searchableBranchesTfNode := "azuredevops_repository_searchable_branches.test"
projectName := testutils.GenerateResourceName()
repoName := testutils.GenerateResourceName()

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
Steps: []resource.TestStep{
{
Config: hclRepoPolicySearchableBranchesResourceBasic(projectName, repoName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(searchableBranchesTfNode, "searchable_branches.#", "1"),
),
}, {
Config: hclRepoPolicySearchableBranchesResourceUpdate(projectName, repoName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(searchableBranchesTfNode, "searchable_branches.#", "2"),
),
}, {
ResourceName: searchableBranchesTfNode,
ImportStateIdFunc: testutils.ComputeProjectQualifiedResourceImportID(searchableBranchesTfNode),
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func hclRepoPolicySearchableBranchesResourceTemplate(projectName string, repoName string) string {
return fmt.Sprintf(`
resource "azuredevops_project" "test" {
name = "%s"
description = "Test Project Description"
visibility = "private"
version_control = "Git"
work_item_template = "Agile"
}

resource "azuredevops_git_repository" "test" {
project_id = azuredevops_project.test.id
name = "%s"
initialization {
init_type = "Clean"
}
}
`, projectName, repoName)
}

func hclRepoPolicySearchableBranchesResourceBasic(projectName string, repoName string) string {
projectAndRepo := hclRepoPolicySearchableBranchesResourceTemplate(projectName, repoName)
return fmt.Sprintf(`
%s

resource "azuredevops_repository_policy_searchable_branches" "test" {
project_id = azuredevops_project.test.id

searchable_branches = ["testbranch"]
repository_ids = [azuredevops_git_repository.test.id]
}`, projectAndRepo)
}

func hclRepoPolicySearchableBranchesResourceUpdate(projectName string, repoName string) string {
projectAndRepo := hclRepoPolicySearchableBranchesResourceTemplate(projectName, repoName)
return fmt.Sprintf(`
%s

resource "azuredevops_repository_policy_searchable_branches" "test" {
project_id = azuredevops_project.test.id

searchable_branches = ["testbranch2"]
repository_ids = [azuredevops_git_repository.test.id]
}`, projectAndRepo)
}
1 change: 1 addition & 0 deletions azuredevops/internal/service/policy/repository/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
PathLength = uuid.MustParse("001a79cf-fda1-4c4e-9e7c-bac40ee5ead8")
FileSize = uuid.MustParse("2e26e725-8201-4edd-8bf5-978563c34a80")
CheckCredentials = uuid.MustParse("e67ae10f-cf9a-40bc-8e66-6b3a8216956e")
SearchableBranches = uuid.MustParse("0517f88d-4ec5-4343-9d26-9930ebd53069")
)

// policyCrudArgs arguments for genBasePolicyResource
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package repository

import (
"maps"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/policy"
)

type searchbranchesPolicySettings struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code can be removed

SearchBranches []string `json:"searchBranches"`
}

func ResourceRepositorySearchableBranches() *schema.Resource {
resource := genBasePolicyResource(&policyCrudArgs{
FlattenFunc: searchableBranchesFlattenFunc,
ExpandFunc: searchableBranchesExpandFunc,
PolicyType: SearchableBranches,
})

maps.Copy(resource.Schema, map[string]*schema.Schema{
"searchable_branches": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"searchable_branches": {
"branches": {

Type: schema.TypeList,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be TypeSet instead of TypeList to avoid potential ordering issues

Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringIsNotEmpty,
},
},
"enabled": {
Type: schema.TypeBool,
Computed: true,
},
"blocking": {
Type: schema.TypeBool,
Computed: true,
},
// API only accepts a single repository as scope.
"repository_ids": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is legacy technical debt. Instead of using a generic scheme, you can add entire schemes as needed and handle create/update/read/delete yourself. Based on the information you shared, it is recommended to do this

Copy link
Author

@mdevreugd mdevreugd Jan 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I just read this comment in order. :)

So you're suggesting to not use the generic schema provided by common.

Let me look into it 👍

Type: schema.TypeList,
Required: true,
MinItems: 1,
MaxItems: 1,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.IsUUID,
},
},
Comment on lines +32 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These properties are handled in the infrastructure and we can remove them

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. The reason why I've added these is to override them, however.

The thing with the Searchable Branches policy is that the API still expects these to be passed, but setting the enabled or blocking to true causes an error.

And you can only scope this for 1 repository, plus I did not want to mess too much with the schema. Hence forcing the repository_ids to accept only 1.

})

return resource
}

func searchableBranchesFlattenFunc(d *schema.ResourceData, policyConfig *policy.PolicyConfiguration, projectID *string) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func searchableBranchesFlattenFunc(d *schema.ResourceData, policyConfig *policy.PolicyConfiguration, projectID *string) error {
func flattenSearchableBranches(d *schema.ResourceData, policyConfig *policy.PolicyConfiguration, projectID *string) error {

err := baseFlattenFunc(d, policyConfig, projectID)
if err != nil {
return err
}

policySettings := policyConfig.Settings.(map[string]interface{})
_ = d.Set("searchable_branches", policySettings["searchBranches"].([]interface{}))
return nil
}

func searchableBranchesExpandFunc(d *schema.ResourceData, typeID uuid.UUID) (*policy.PolicyConfiguration, *string, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func searchableBranchesExpandFunc(d *schema.ResourceData, typeID uuid.UUID) (*policy.PolicyConfiguration, *string, error) {
func expandSearchableBranches(d *schema.ResourceData, typeID uuid.UUID) (*policy.PolicyConfiguration, *string, error) {

policyConfig, projectID, err := baseExpandFunc(d, typeID)
if err != nil {
return nil, nil, err
}

policySettings := policyConfig.Settings.(map[string]interface{})
policySettings["searchBranches"] = d.Get("searchable_branches")

// Overriding blocking and enabled as it has no use for this policy setting
policySettings["blocking"] = false
policySettings["enabled"] = false

return policyConfig, projectID, nil
}
1 change: 1 addition & 0 deletions azuredevops/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func Provider() *schema.Provider {
"azuredevops_repository_policy_max_file_size": repository.ResourceRepositoryMaxFileSize(),
"azuredevops_repository_policy_max_path_length": repository.ResourceRepositoryMaxPathLength(),
"azuredevops_repository_policy_reserved_names": repository.ResourceRepositoryReservedNames(),
"azuredevops_repository_policy_searchable_branches": repository.ResourceRepositorySearchableBranches(),
"azuredevops_resource_authorization": build.ResourceResourceAuthorization(),
"azuredevops_securityrole_assignment": securityroles.ResourceSecurityRoleAssignment(),
"azuredevops_serviceendpoint_argocd": serviceendpoint.ResourceServiceEndpointArgoCD(),
Expand Down
1 change: 1 addition & 0 deletions azuredevops/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func TestProvider_HasChildResources(t *testing.T) {
"azuredevops_repository_policy_max_file_size",
"azuredevops_repository_policy_max_path_length",
"azuredevops_repository_policy_reserved_names",
"azuredevops_repository_policy_searchable_branches",
"azuredevops_resource_authorization",
"azuredevops_securityrole_assignment",
"azuredevops_serviceendpoint_argocd",
Expand Down
64 changes: 64 additions & 0 deletions website/docs/r/repository_policy_searchable_branches.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
layout: "azuredevops"
page_title: "AzureDevops: azuredevops_repository_policy_searchable_branches"
description: |- Manages searchable branches repository policy within Azure DevOps project.
---

# azuredevops_repository_policy_searchable_branches

Manage searchable branches repository policy within Azure DevOps project.

## Example Usage

```hcl
resource "azuredevops_project" "example" {
name = "Example Project"
visibility = "private"
version_control = "Git"
work_item_template = "Agile"
description = "Managed by Terraform"
}

resource "azuredevops_git_repository" "example" {
project_id = azuredevops_project.example.id
name = "Example Repository"
initialization {
init_type = "Clean"
}
}

resource "azuredevops_repository_policy_searchable_branches" "example" {
project_id = data.azuredevops_project.example.id
searchable_branches = ["examplebranch"]
repository_ids = [data.azuredevops_git_repository.example.id]

}
```

## Argument Reference

The following arguments are supported:

- `project_id` - (Required) The ID of the project in which the policy will be created.
- `enabled` - (Computed) This is set to false by the provider and is not used.
- `blocking` - (Computed) This is set to false by the provider and is not used.
- `searchable_branches` - (Required) A list of branch names to be added as searchable branches for the repository. Branches do not have to exist.
- `repository_ids` (Required) ID of repository for which the policy is enabled. Note: Due to the API implementation of this policy, this only accepts 1 repository id.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

- `id` - The ID of the repository policy.

## Relevant Links

- [Azure DevOps Service REST API 7.0 - Policy Configurations](https://docs.microsoft.com/en-us/rest/api/azure/devops/policy/configurations?view=azure-devops-rest-7.0)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Timeouts
The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions:
* `create` - (Defaults to 30 minutes) Used when creating the Searchable Branches Repository Policy.
* `read` - (Defaults to 5 minute) Used when retrieving the Searchable Branches Repository Policy.
* `update` - (Defaults to 30 minutes) Used when updating the Searchable Branches Repository Policy.
* `delete` - (Defaults to 30 minutes) Used when deleting the Searchable Branches Repository Policy.

## Import

Azure DevOps repository policies can be imported using the projectID/policyID or projectName/policyID:

```sh
terraform import azuredevops_repository_policy_searchable_branches.example 00000000-0000-0000-0000-000000000000/0
```