summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxuzhang3 <57888764+xuzhang3@users.noreply.github.com>2022-08-05 11:30:51 +0800
committerGitHub <noreply@github.com>2022-08-05 11:30:51 +0800
commit805e11022bab9b05aec296bde266a95b7881f5d5 (patch)
tree77b74e91e3444c60cac6431aa2ecba8b4d3ba9db
parentcef29ccde2435059f650ed87ec7bcabead08ce09 (diff)
parent8672e4438179299501642a9654fd22f1c3ea0567 (diff)
Merge pull request #604 from bobmhong/feature/build_folder_resource
add resources build_folder & build_folder_permissions
-rw-r--r--azuredevops/internal/acceptancetests/resource_build_folder_permissions_test.go137
-rw-r--r--azuredevops/internal/acceptancetests/resource_build_folder_test.go35
-rw-r--r--azuredevops/internal/acceptancetests/testutils/hcl.go16
-rw-r--r--azuredevops/internal/service/build/resource_build_folder.go184
-rw-r--r--azuredevops/internal/service/build/resource_build_folder_test.go144
-rw-r--r--azuredevops/internal/service/permissions/resource_build_folder_permissions.go124
-rw-r--r--azuredevops/internal/service/permissions/resource_build_folder_permissions_test.go117
-rw-r--r--azuredevops/provider.go2
-rw-r--r--azuredevops/provider_test.go2
-rw-r--r--main.go9
-rw-r--r--website/azuredevops.erb6
-rw-r--r--website/docs/r/build_folder.html.markdown49
-rw-r--r--website/docs/r/build_folder_permissions.html.markdown98
13 files changed, 923 insertions, 0 deletions
diff --git a/azuredevops/internal/acceptancetests/resource_build_folder_permissions_test.go b/azuredevops/internal/acceptancetests/resource_build_folder_permissions_test.go
new file mode 100644
index 00000000..f37640ba
--- /dev/null
+++ b/azuredevops/internal/acceptancetests/resource_build_folder_permissions_test.go
@@ -0,0 +1,137 @@
+//go:build (all || permissions || resource_build_Folder_permissions) && (!exclude_permissions || !exclude_resource_build_Folder_permissions)
+// +build all permissions resource_build_Folder_permissions
+// +build !exclude_permissions !exclude_resource_build_Folder_permissions
+
+package acceptancetests
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/acceptancetests/testutils"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/datahelper"
+)
+
+func hclBuildFolderPermissions(projectName string, path string, permissions map[string]string) string {
+ rootPermissions := datahelper.JoinMap(permissions, "=", "\n")
+ description := "Integration Test Folder"
+
+ return fmt.Sprintf(`
+%s
+
+data "azuredevops_group" "tf-project-readers" {
+ project_id = azuredevops_project.project.id
+ name = "Readers"
+}
+
+resource "azuredevops_build_folder_permissions" "permissions" {
+ project_id = azuredevops_project.project.id
+ principal = data.azuredevops_group.tf-project-readers.id
+ path = azuredevops_build_folder.test_folder.path
+
+ permissions = {
+ %s
+ }
+}
+`,
+ testutils.HclBuildFolder(projectName, path, description),
+ rootPermissions,
+ )
+}
+
+func TestAccBuildFolderPermissions_SetPermissions(t *testing.T) {
+ projectName := testutils.GenerateResourceName()
+ config := hclBuildFolderPermissions(projectName, `\test-folder`, map[string]string{
+ "ViewBuilds": "Allow",
+ "EditBuildQuality": "Allow",
+ "RetainIndefinitely": "Allow",
+ "DeleteBuilds": "Deny",
+ "ManageBuildQualities": "Allow",
+ "DestroyBuilds": "Allow",
+ "UpdateBuildInformation": "Allow",
+ "QueueBuilds": "Allow",
+ "ManageBuildQueue": "Allow",
+ "StopBuilds": "Allow",
+ "ViewBuildDefinition": "Allow",
+ "EditBuildDefinition": "Allow",
+ "DeleteBuildDefinition": "Deny",
+ "AdministerBuildPermissions": "NotSet",
+ })
+ tfNodeRoot := "azuredevops_build_folder_permissions.permissions"
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { testutils.PreCheck(t, nil) },
+ Providers: testutils.GetProviders(),
+ CheckDestroy: testutils.CheckProjectDestroyed,
+ Steps: []resource.TestStep{
+ {
+ Config: config,
+ Check: resource.ComposeTestCheckFunc(
+ testutils.CheckProjectExists(projectName),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "project_id"),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "principal"),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "path"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.%", "14"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.ViewBuilds", "allow"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.DeleteBuilds", "deny"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.DeleteBuildDefinition", "deny"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.AdministerBuildPermissions", "notset"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccBuildFolderPermissions_UpdatePermissions(t *testing.T) {
+ projectName := testutils.GenerateResourceName()
+ config1 := hclBuildFolderPermissions(projectName, `\dir1`, map[string]string{
+ "ViewBuilds": "Deny",
+ "EditBuildQuality": "NotSet",
+ "RetainIndefinitely": "Deny",
+ "DeleteBuilds": "Deny",
+ })
+ config2 := hclBuildFolderPermissions(projectName, `\dir1`, map[string]string{
+ "ViewBuilds": "Deny",
+ "EditBuildQuality": "Allow",
+ "RetainIndefinitely": "Deny",
+ "DeleteBuilds": "Deny",
+ })
+ tfNodeRoot := "azuredevops_build_folder_permissions.permissions"
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { testutils.PreCheck(t, nil) },
+ Providers: testutils.GetProviders(),
+ CheckDestroy: testutils.CheckProjectDestroyed,
+ Steps: []resource.TestStep{
+ {
+ Config: config1,
+ Check: resource.ComposeTestCheckFunc(
+ testutils.CheckProjectExists(projectName),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "project_id"),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "principal"),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "path"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.%", "4"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.ViewBuilds", "deny"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.EditBuildQuality", "notset"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.RetainIndefinitely", "deny"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.DeleteBuilds", "deny"),
+ ),
+ },
+ {
+ Config: config2,
+ Check: resource.ComposeTestCheckFunc(
+ testutils.CheckProjectExists(projectName),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "project_id"),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "principal"),
+ resource.TestCheckResourceAttrSet(tfNodeRoot, "path"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.%", "4"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.ViewBuilds", "deny"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.EditBuildQuality", "allow"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.RetainIndefinitely", "deny"),
+ resource.TestCheckResourceAttr(tfNodeRoot, "permissions.DeleteBuilds", "deny"),
+ ),
+ },
+ },
+ })
+}
diff --git a/azuredevops/internal/acceptancetests/resource_build_folder_test.go b/azuredevops/internal/acceptancetests/resource_build_folder_test.go
new file mode 100644
index 00000000..112fff3f
--- /dev/null
+++ b/azuredevops/internal/acceptancetests/resource_build_folder_test.go
@@ -0,0 +1,35 @@
+//go:build (all || resource_build_folder) && (!exclude_permissions || !exclude_resource_build_folder)
+// +build all resource_build_folder
+// +build !exclude_permissions !exclude_resource_build_folder
+
+package acceptancetests
+
+import (
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/acceptancetests/testutils"
+)
+
+func TestAccBuildFolder(t *testing.T) {
+ projectName := testutils.GenerateResourceName()
+ config := testutils.HclBuildFolder(projectName, "\\test-folder", "Acceptance Test Folder")
+
+ tfNode := "azuredevops_build_folder.test_folder"
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { testutils.PreCheck(t, nil) },
+ Providers: testutils.GetProviders(),
+ CheckDestroy: testutils.CheckProjectDestroyed,
+ Steps: []resource.TestStep{
+ {
+ Config: config,
+ Check: resource.ComposeTestCheckFunc(
+ testutils.CheckProjectExists(projectName),
+ resource.TestCheckResourceAttrSet(tfNode, "project_id"),
+ resource.TestCheckResourceAttrSet(tfNode, "path"),
+ resource.TestCheckResourceAttrSet(tfNode, "description"),
+ ),
+ },
+ },
+ })
+}
diff --git a/azuredevops/internal/acceptancetests/testutils/hcl.go b/azuredevops/internal/acceptancetests/testutils/hcl.go
index bfa916d1..89e5f5ad 100644
--- a/azuredevops/internal/acceptancetests/testutils/hcl.go
+++ b/azuredevops/internal/acceptancetests/testutils/hcl.go
@@ -897,6 +897,22 @@ resource "azuredevops_project_permissions" "project-permissions" {
`, projectResource)
}
+// HclBuildFolder creates HCL for testing Build Folders
+func HclBuildFolder(projectName string, path string, description string) string {
+ projectResource := HclProjectResource(projectName)
+
+ escapedBuildPath := strings.ReplaceAll(path, `\`, `\\`)
+ return fmt.Sprintf(`
+%s
+
+resource "azuredevops_build_folder" "test_folder" {
+ project_id = azuredevops_project.project.id
+ path = "%s"
+ description = "%s"
+}
+`, projectResource, escapedBuildPath, description)
+}
+
// HclGitPermissions creates HCl for testing to set permissions for a the all Git repositories of AzDO project
func HclGitPermissions(projectName string) string {
projectResource := HclProjectResource(projectName)
diff --git a/azuredevops/internal/service/build/resource_build_folder.go b/azuredevops/internal/service/build/resource_build_folder.go
new file mode 100644
index 00000000..b9dc9e91
--- /dev/null
+++ b/azuredevops/internal/service/build/resource_build_folder.go
@@ -0,0 +1,184 @@
+package build
+
+import (
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/build"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/core"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/tfhelper"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/validate"
+)
+
+// ResourceBuildFolder schema and implementation for build folder resource
+func ResourceBuildFolder() *schema.Resource {
+ return &schema.Resource{
+ Create: resourceBuildFolderCreate,
+ Read: resourceBuildFolderRead,
+ Update: resourceBuildFolderUpdate,
+ Delete: resourceBuildFolderDelete,
+ Importer: tfhelper.ImportProjectQualifiedResource(),
+ Schema: map[string]*schema.Schema{
+ "project_id": {
+ Type: schema.TypeString,
+ Required: true,
+ ForceNew: true,
+ },
+ "path": {
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: validate.Path,
+ },
+ "description": {
+ Type: schema.TypeString,
+ Optional: true,
+ Default: ``,
+ },
+ },
+ }
+}
+
+func resourceBuildFolderCreate(d *schema.ResourceData, m interface{}) error {
+ clients := m.(*client.AggregatedClient)
+
+ projectID := d.Get("project_id").(string)
+ description := d.Get("description").(string)
+ path := d.Get("path").(string)
+
+ createdBuildFolder, err := createBuildFolder(clients, path, projectID, description)
+ if err != nil {
+ return fmt.Errorf(" failed creating resource Build Folder, project ID: %s. Error: %+v", projectID, err)
+ }
+
+ flattenBuildFolder(d, createdBuildFolder, projectID)
+ return resourceBuildFolderRead(d, m)
+}
+
+func resourceBuildFolderRead(d *schema.ResourceData, m interface{}) error {
+ clients := m.(*client.AggregatedClient)
+
+ projectID := d.Get("project_id").(string)
+ path := d.Id()
+
+ buildFolders, err := clients.BuildClient.GetFolders(clients.Ctx, build.GetFoldersArgs{
+ Project: &projectID,
+ Path: &path,
+ })
+
+ if err != nil {
+ if utils.ResponseWasNotFound(err) {
+ d.SetId("")
+ return nil
+ }
+ return err
+ }
+
+ if len(*buildFolders) == 0 {
+ d.SetId("")
+ log.Printf("[TRACE] plugin.terraform-provider-azuredevops: Folder [%s] not found. Removing from state.", path)
+ return nil
+ }
+
+ buildFolder := (*buildFolders)[0]
+
+ flattenBuildFolder(d, &buildFolder, projectID)
+ return nil
+}
+
+func resourceBuildFolderUpdate(d *schema.ResourceData, m interface{}) error {
+ clients := m.(*client.AggregatedClient)
+
+ path := d.Get("path").(string)
+ buildFolder, projectID, err := expandBuildFolder(d)
+ if err != nil {
+ return fmt.Errorf(" failed to expand build folder configurations. Project ID: %s , Error: %+v", projectID, err)
+ }
+
+ updatedBuildFolder, err := clients.BuildClient.UpdateFolder(m.(*client.AggregatedClient).Ctx, build.UpdateFolderArgs{
+ Project: &projectID,
+ Path: &path,
+ Folder: buildFolder,
+ })
+
+ if err != nil {
+ return fmt.Errorf("failed to update build folder. Project ID: %s, Error: %+v ", projectID, err)
+ }
+
+ flattenBuildFolder(d, updatedBuildFolder, projectID)
+ return resourceBuildFolderRead(d, m)
+}
+
+func resourceBuildFolderDelete(d *schema.ResourceData, m interface{}) error {
+ if strings.EqualFold(d.Id(), "") {
+ return nil
+ }
+
+ clients := m.(*client.AggregatedClient)
+
+ projectID := d.Get("project_id").(string)
+ path := d.Get("path").(string)
+
+ err := clients.BuildClient.DeleteFolder(m.(*client.AggregatedClient).Ctx, build.DeleteFolderArgs{
+ Project: &projectID,
+ Path: &path,
+ })
+
+ return err
+}
+
+func flattenBuildFolder(d *schema.ResourceData, buildFolder *build.Folder, projectID string) {
+ d.SetId(*buildFolder.Path)
+ d.Set("project_id", projectID)
+ d.Set("path", buildFolder.Path)
+ d.Set("description", buildFolder.Description)
+}
+
+// create a Folder object to pass to the API
+func createBuildFolder(clients *client.AggregatedClient, path string, project string, description string) (*build.Folder, error) {
+ projectUuid, err := uuid.Parse(project)
+ if err != nil {
+ return nil, err
+ }
+
+ createdBuild, err := clients.BuildClient.CreateFolder(clients.Ctx, build.CreateFolderArgs{
+ Folder: &build.Folder{
+ Description: &description,
+ Path: &path,
+ Project: &core.TeamProjectReference{
+ Id: &projectUuid,
+ },
+ },
+ Project: &project,
+ Path: &path,
+ })
+
+ return createdBuild, err
+}
+
+// create a Folder object from the tf Resource Data
+func expandBuildFolder(d *schema.ResourceData) (*build.Folder, string, error) {
+ projectID := d.Get("project_id").(string)
+
+ projectUuid, err := uuid.Parse(projectID)
+ if err != nil {
+ return nil, "", err
+ }
+
+ projectReference := core.TeamProjectReference{
+ Id: &projectUuid,
+ }
+
+ buildFolder := build.Folder{
+ Description: converter.String(d.Get("description").(string)),
+ Path: converter.String(d.Get("path").(string)),
+ Project: &projectReference,
+ }
+
+ return &buildFolder, projectID, nil
+}
diff --git a/azuredevops/internal/service/build/resource_build_folder_test.go b/azuredevops/internal/service/build/resource_build_folder_test.go
new file mode 100644
index 00000000..10f3f7c7
--- /dev/null
+++ b/azuredevops/internal/service/build/resource_build_folder_test.go
@@ -0,0 +1,144 @@
+//go:build (all || resource_build_folder) && !exclude_resource_build_folder
+// +build all resource_build_folder
+// +build !exclude_resource_build_folder
+
+package build
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/build"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/core"
+ "github.com/microsoft/terraform-provider-azuredevops/azdosdkmocks"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter"
+ "github.com/stretchr/testify/require"
+)
+
+var testProjectUUID = uuid.New()
+var testProjectReference = core.TeamProjectReference{
+ Id: &testProjectUUID,
+}
+
+var testPath = "\\"
+
+// This definition matches the overall structure of what a configured folder would look like
+var testBuildFolder = build.Folder{
+ Description: converter.String("My Folder Description"),
+ Path: converter.String(testPath),
+ Project: &testProjectReference,
+}
+
+// validates that an error is thrown if any of the un-supported path characters are used
+func TestBuildFolder_PathInvalidCharacterListIsError(t *testing.T) {
+ expectedInvalidPathCharacters := []string{"<", ">", "|", ":", "$", "@", "\"", "/", "%", "+", "*", "?"}
+ pathSchema := ResourceBuildFolder().Schema["path"]
+
+ for _, invalidCharacter := range expectedInvalidPathCharacters {
+ _, errors := pathSchema.ValidateFunc(`\`+invalidCharacter, "")
+ require.Equal(t, "<>|:$@\"/%+*? are not allowed in path", errors[0].Error())
+ }
+}
+
+// validates that an error is thrown if path does not start with slash
+func TestBuildFolder_PathInvalidStartingSlashIsError(t *testing.T) {
+ pathSchema := ResourceBuildFolder().Schema["path"]
+ _, errors := pathSchema.ValidateFunc("dir\\dir", "")
+ require.Equal(t, "path must start with backslash", errors[0].Error())
+}
+
+// verifies that an expand will fail if there is insufficient configuration data found in the resource
+func TestBuildFolder_Expand_FailsIfNotEnoughData(t *testing.T) {
+ resourceData := schema.TestResourceDataRaw(t, ResourceBuildFolder().Schema, nil)
+ _, _, err := expandBuildFolder(resourceData)
+ require.NotNil(t, err)
+}
+
+// verifies that if an error is produced on create, the error is not swallowed
+func TestBuildFolder_Create_DoesNotSwallowError(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ resourceData := schema.TestResourceDataRaw(t, ResourceBuildFolder().Schema, nil)
+ flattenBuildFolder(resourceData, &testBuildFolder, testProjectID)
+
+ buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
+ clients := &client.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
+
+ buildClient.
+ EXPECT().
+ CreateFolder(clients.Ctx, gomock.Any()).
+ Return(nil, errors.New("CreateFolder() Failed")).
+ Times(1)
+
+ err := resourceBuildFolderCreate(resourceData, clients)
+ require.Contains(t, err.Error(), "CreateFolder() Failed")
+}
+
+// verifies that if an error is produced on a read, it is not swallowed
+func TestBuildFolder_Read_DoesNotSwallowError(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ resourceData := schema.TestResourceDataRaw(t, ResourceBuildFolder().Schema, nil)
+ flattenBuildFolder(resourceData, &testBuildFolder, testProjectID)
+
+ buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
+ clients := &client.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
+
+ buildClient.
+ EXPECT().
+ GetFolders(clients.Ctx, gomock.Any()).
+ Return(nil, errors.New("GetFolder() Failed")).
+ Times(1)
+
+ err := resourceBuildFolderRead(resourceData, clients)
+ require.Equal(t, "GetFolder() Failed", err.Error())
+}
+
+// verifies that if an error is produced on a delete, it is not swallowed
+func TestBuildFolder_Delete_DoesNotSwallowError(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ resourceData := schema.TestResourceDataRaw(t, ResourceBuildFolder().Schema, nil)
+ flattenBuildFolder(resourceData, &testBuildFolder, testProjectID)
+
+ buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
+ clients := &client.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
+
+ buildClient.
+ EXPECT().
+ DeleteFolder(clients.Ctx, gomock.Any()).
+ Return(errors.New("DeleteFolder() Failed")).
+ Times(1)
+
+ err := resourceBuildFolderDelete(resourceData, clients)
+ require.Equal(t, "DeleteFolder() Failed", err.Error())
+}
+
+// verifies that if an error is produced on an update, it is not swallowed
+func TestBuildFolder_Update_DoesNotSwallowError(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ resourceData := schema.TestResourceDataRaw(t, ResourceBuildFolder().Schema, nil)
+ flattenBuildFolder(resourceData, &testBuildFolder, testProjectID)
+
+ buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
+ clients := &client.AggregatedClient{BuildClient: buildClient, Ctx: context.Background()}
+
+ buildClient.
+ EXPECT().
+ UpdateFolder(clients.Ctx, gomock.Any()).
+ Return(nil, errors.New("UpdateFolder() Failed")).
+ Times(1)
+
+ err := resourceBuildFolderUpdate(resourceData, clients)
+ require.Contains(t, err.Error(), "UpdateFolder() Failed")
+}
diff --git a/azuredevops/internal/service/permissions/resource_build_folder_permissions.go b/azuredevops/internal/service/permissions/resource_build_folder_permissions.go
new file mode 100644
index 00000000..06890950
--- /dev/null
+++ b/azuredevops/internal/service/permissions/resource_build_folder_permissions.go
@@ -0,0 +1,124 @@
+package permissions
+
+import (
+ "fmt"
+ "log"
+
+ "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/v6/build"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
+ securityhelper "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/permissions/utils"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter"
+)
+
+// ResourceBuildFolderPermissions schema and implementation for build permission resource
+func ResourceBuildFolderPermissions() *schema.Resource {
+ return &schema.Resource{
+ Create: resourceBuildFolderPermissionsCreateOrUpdate,
+ Read: resourceBuildFolderPermissionsRead,
+ Update: resourceBuildFolderPermissionsCreateOrUpdate,
+ Delete: resourceBuildFolderPermissionsDelete,
+ Schema: securityhelper.CreatePermissionResourceSchema(map[string]*schema.Schema{
+ "project_id": {
+ Type: schema.TypeString,
+ ValidateFunc: validation.IsUUID,
+ Required: true,
+ ForceNew: true,
+ },
+ "path": {
+ Type: schema.TypeString,
+ Required: true,
+ ForceNew: true,
+ },
+ }),
+ }
+}
+
+func resourceBuildFolderPermissionsCreateOrUpdate(d *schema.ResourceData, m interface{}) error {
+ clients := m.(*client.AggregatedClient)
+
+ sn, err := securityhelper.NewSecurityNamespace(d, clients, securityhelper.SecurityNamespaceIDValues.Build, createBuildFolderToken)
+ if err != nil {
+ return err
+ }
+
+ if err := securityhelper.SetPrincipalPermissions(d, sn, nil, false); err != nil {
+ return err
+ }
+
+ return resourceBuildFolderPermissionsRead(d, m)
+}
+
+func resourceBuildFolderPermissionsRead(d *schema.ResourceData, m interface{}) error {
+ clients := m.(*client.AggregatedClient)
+
+ sn, err := securityhelper.NewSecurityNamespace(d, clients, securityhelper.SecurityNamespaceIDValues.Build, createBuildFolderToken)
+ if err != nil {
+ return err
+ }
+
+ principalPermissions, err := securityhelper.GetPrincipalPermissions(d, sn)
+ if err != nil {
+ return err
+ }
+ if principalPermissions == nil {
+ d.SetId("")
+ log.Printf("[INFO] Permissions for ACL token %q not found. Removing from state", sn.GetToken())
+ return nil
+ }
+
+ d.Set("permissions", principalPermissions.Permissions)
+ return nil
+}
+
+func resourceBuildFolderPermissionsDelete(d *schema.ResourceData, m interface{}) error {
+ clients := m.(*client.AggregatedClient)
+
+ sn, err := securityhelper.NewSecurityNamespace(d, clients, securityhelper.SecurityNamespaceIDValues.Build, createBuildFolderToken)
+ if err != nil {
+ return err
+ }
+
+ if err := securityhelper.SetPrincipalPermissions(d, sn, &securityhelper.PermissionTypeValues.NotSet, true); err != nil {
+ return err
+ }
+ d.SetId("")
+ return nil
+}
+
+func createBuildFolderToken(d *schema.ResourceData, clients *client.AggregatedClient) (string, error) {
+ projectID, ok := d.GetOk("project_id")
+ if !ok {
+ return "", fmt.Errorf("Failed to get 'project_id' from schema")
+ }
+
+ buildFolderPath, ok := d.GetOk("path")
+ if !ok {
+ return "", fmt.Errorf("Failed to get 'path' from schema")
+ }
+
+ buildFolders, err := clients.BuildClient.GetFolders(clients.Ctx, build.GetFoldersArgs{
+ Project: converter.String(projectID.(string)),
+ Path: converter.String(buildFolderPath.(string)),
+ })
+
+ if err != nil {
+ return "", fmt.Errorf(" failed to get the folder. Project ID: %s, path: %s. %+v", projectID, buildFolderPath, err)
+ }
+
+ Folder := (*buildFolders)[0]
+
+ var aclToken string
+
+ // The token format is Project_ID/Path
+ if *Folder.Path != "\\" {
+ transformedPath := transformPath(*Folder.Path)
+
+ aclToken = fmt.Sprintf("%s/%s", projectID.(string), transformedPath)
+ } else {
+ aclToken = fmt.Sprintf("%s/%s", projectID.(string), *Folder.Path)
+ }
+
+ return aclToken, nil
+}
diff --git a/azuredevops/internal/service/permissions/resource_build_folder_permissions_test.go b/azuredevops/internal/service/permissions/resource_build_folder_permissions_test.go
new file mode 100644
index 00000000..cb302240
--- /dev/null
+++ b/azuredevops/internal/service/permissions/resource_build_folder_permissions_test.go
@@ -0,0 +1,117 @@
+//go:build (all || permissions || resource_build_folder_permissions) && (!exclude_permissions || !resource_build_folder_permissions)
+// +build all permissions resource_build_folder_permissions
+// +build !exclude_permissions !resource_build_folder_permissions
+
+package permissions
+
+// The tests in this file use the mock clients in mock_client.go to mock out
+// the Azure DevOps client operations.
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/microsoft/azure-devops-go-api/azuredevops/v6/build"
+ "github.com/microsoft/terraform-provider-azuredevops/azdosdkmocks"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
+ "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter"
+ "github.com/stretchr/testify/assert"
+)
+
+/**
+ * Begin unit tests
+ */
+
+var buildFolderProjectID = "9083e944-8e9e-405e-960a-c80180aa71e6"
+
+var buildFolderToken = fmt.Sprintf("%s", buildFolderProjectID)
+
+var buildFolderPath = "a/b/c"
+var buildFolderTokenPath = fmt.Sprintf("%s/%s", buildFolderProjectID, buildFolderPath)
+
+func TestBuildFolderPermissions_CreateBuildFolderToken(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
+ clients := &client.AggregatedClient{
+ BuildClient: buildClient,
+ Ctx: context.Background(),
+ }
+
+ folder := build.Folder{
+ Description: converter.String("Test Folder"),
+ Path: converter.String("\\"),
+ }
+
+ mockFolders := []build.Folder{folder}
+
+ buildClient.EXPECT().
+ GetFolders(clients.Ctx, gomock.Any()).
+ Return(&mockFolders, nil).
+ Times(1)
+
+ var d *schema.ResourceData
+ var token string
+ var err error
+
+ d = getBuildFolderPermissionsResource(t, buildFolderProjectID, "\\")
+ token, err = createBuildFolderToken(d, clients)
+ assert.NotEmpty(t, token)
+ assert.Nil(t, err)
+ assert.Equal(t, "9083e944-8e9e-405e-960a-c80180aa71e6/\\", token)
+
+ d = getBuildFolderPermissionsResource(t, "", "")
+ token, err = createBuildFolderToken(d, clients)
+ assert.Empty(t, token)
+ assert.NotNil(t, err)
+}
+
+func TestBuildFolderPermissions_CreateBuildTokenWithPaths(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ buildClient := azdosdkmocks.NewMockBuildClient(ctrl)
+ clients := &client.AggregatedClient{
+ BuildClient: buildClient,
+ Ctx: context.Background(),
+ }
+
+ path := "\\a\\b\\c"
+
+ folder := build.Folder{
+ Description: converter.String("Test Folder"),
+ Path: converter.String(path),
+ }
+
+ mockFolders := []build.Folder{folder}
+
+ buildClient.EXPECT().
+ GetFolders(clients.Ctx, gomock.Any()).
+ Return(&mockFolders, nil).
+ Times(1)
+
+ var d *schema.ResourceData
+ var token string
+ var err error
+
+ d = getBuildFolderPermissionsResource(t, buildFolderProjectID, path)
+ token, err = createBuildFolderToken(d, clients)
+ assert.NotEmpty(t, token)
+ assert.Nil(t, err)
+ assert.Equal(t, buildFolderTokenPath, token)
+}
+
+func getBuildFolderPermissionsResource(t *testing.T, projectID string, buildFolderPath string) *schema.ResourceData {
+ d := schema.TestResourceDataRaw(t, ResourceBuildFolderPermissions().Schema, nil)
+ if projectID != "" {
+ d.Set("project_id", projectID)
+ }
+ if buildFolderPath != "" {
+ d.Set("path", buildFolderPath)
+ }
+ return d
+}
diff --git a/azuredevops/provider.go b/azuredevops/provider.go
index b98c618f..02776696 100644
--- a/azuredevops/provider.go
+++ b/azuredevops/provider.go
@@ -33,6 +33,7 @@ func Provider() *schema.Provider {
"azuredevops_branch_policy_merge_types": branch.ResourceBranchPolicyMergeTypes(),
"azuredevops_branch_policy_status_check": branch.ResourceBranchPolicyStatusCheck(),
"azuredevops_build_definition": build.ResourceBuildDefinition(),
+ "azuredevops_build_folder": build.ResourceBuildFolder(),
"azuredevops_project": core.ResourceProject(),
"azuredevops_project_features": core.ResourceProjectFeatures(),
"azuredevops_project_pipeline_settings": core.ResourceProjectPipelineSettings(),
@@ -77,6 +78,7 @@ func Provider() *schema.Provider {
"azuredevops_area_permissions": permissions.ResourceAreaPermissions(),
"azuredevops_iteration_permissions": permissions.ResourceIterationPermissions(),
"azuredevops_build_definition_permissions": permissions.ResourceBuildDefinitionPermissions(),
+ "azuredevops_build_folder_permissions": permissions.ResourceBuildFolderPermissions(),
"azuredevops_team": core.ResourceTeam(),
"azuredevops_team_members": core.ResourceTeamMembers(),
"azuredevops_team_administrators": core.ResourceTeamAdministrators(),
diff --git a/azuredevops/provider_test.go b/azuredevops/provider_test.go
index d14edeba..6b642542 100644
--- a/azuredevops/provider_test.go
+++ b/azuredevops/provider_test.go
@@ -69,6 +69,8 @@ func TestProvider_HasChildResources(t *testing.T) {
"azuredevops_servicehook_permissions",
"azuredevops_tagging_permissions",
"azuredevops_environment",
+ "azuredevops_build_folder",
+ "azuredevops_build_folder_permissions",
}
resources := Provider().ResourcesMap
diff --git a/main.go b/main.go
index 90c270af..6b370faf 100644
--- a/main.go
+++ b/main.go
@@ -1,13 +1,22 @@
package main
import (
+ "flag"
+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops"
)
func main() {
+ var debug bool
+
+ flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
+ flag.Parse()
+
plugin.Serve(&plugin.ServeOpts{
+ Debug: debug,
+ ProviderAddr: "registry.terraform.io/microsoft/azuredevops",
ProviderFunc: func() *schema.Provider {
return azuredevops.Provider()
},
diff --git a/website/azuredevops.erb b/website/azuredevops.erb
index 52e64605..a3278676 100644
--- a/website/azuredevops.erb
+++ b/website/azuredevops.erb
@@ -125,6 +125,12 @@
<a href="/docs/providers/azuredevops/r/build_definition.html">azuredevops_build_definition</a>
</li>
<li>
+ <a href="/docs/providers/azuredevops/r/build_folder_permissions.html">azuredevops_build_folder_permissions</a>
+ </li>
+ <li>
+ <a href="/docs/providers/azuredevops/r/build_folder.html">azuredevops_build_folder</a>
+ </li>
+ <li>
<a href="/docs/providers/azuredevops/r/git_permissions.html">azuredevops_git_permissions</a>
</li>
<li>
diff --git a/website/docs/r/build_folder.html.markdown b/website/docs/r/build_folder.html.markdown
new file mode 100644
index 00000000..8dc012fb
--- /dev/null
+++ b/website/docs/r/build_folder.html.markdown
@@ -0,0 +1,49 @@
+---
+layout: "azuredevops"
+page_title: "AzureDevops: azuredevops_build_folder"
+description: |-
+ Manages a Build Folder.
+---
+
+# azuredevops_build_folder
+
+Manages a Build Folder.
+
+## Example Usage
+
+```hcl
+resource "azuredevops_project" "example" {
+ name = "Example Project"
+ visibility = "private"
+ version_control = "Git"
+ work_item_template = "Agile"
+}
+
+resource "azuredevops_build_folder" "example" {
+ project_id = azuredevops_project.example.id
+ path = "\\ExampleFolder"
+ description = "ExampleFolder description"
+}
+```
+
+## Arguments Reference
+
+The following arguments are supported:
+
+* `project_id` - (Required) The ID of the project in which the folder will be created.
+* `path` - (Required) The folder path.
+* `description` - (Optional) Folder Description.
+
+## Import
+
+Build Folders can be imported using the `project name/path` or `project id/path`, e.g.
+
+```shell
+terraform import azuredevops_build_folder.example "Example Project/\\ExampleFolder"
+```
+
+or
+
+```shell
+terraform import azuredevops_build_folder.example 00000000-0000-0000-0000-000000000000/\\ExampleFolder
+```
diff --git a/website/docs/r/build_folder_permissions.html.markdown b/website/docs/r/build_folder_permissions.html.markdown
new file mode 100644
index 00000000..f6f05a07
--- /dev/null
+++ b/website/docs/r/build_folder_permissions.html.markdown
@@ -0,0 +1,98 @@
+---
+layout: "azuredevops"
+page_title: "AzureDevops: azuredevops_build_folder_permissions"
+description: |-
+ Manages permissions for a AzureDevOps Build Folder
+---
+
+# azuredevops_build_folder_permissions
+
+Manages permissions for a Build Folder
+
+~> **Note** Permissions can be assigned to group principals and not to single user principals.
+
+## Example Usage
+
+```hcl
+resource "azuredevops_project" "example" {
+ name = "Example Project"
+ work_item_template = "Agile"
+ version_control = "Git"
+ visibility = "private"
+ description = "Managed by Terraform"
+}
+
+data "azuredevops_group" "example-readers" {
+ project_id = azuredevops_project.example.id
+ name = "Readers"
+}
+
+resource "azuredevops_build_folder" "example" {
+ project_id = azuredevops_project.example.id
+ path = "\\ExampleFolder"
+ description = "ExampleFolder description"
+}
+
+resource "azuredevops_build_folder_permissions" "example" {
+ project_id = azuredevops_project.example.id
+ path = "\\ExampleFolder"
+ principal = data.azuredevops_group.example-readers.id
+
+ permissions = {
+ "ViewBuilds": "Allow",
+ "EditBuildQuality": "Allow",
+ "RetainIndefinitely": "Allow",
+ "DeleteBuilds": "Deny",
+ "ManageBuildQualities": "Deny",
+ "DestroyBuilds": "Deny",
+ "UpdateBuildInformation": "Deny",
+ "QueueBuilds": "Allow",
+ "ManageBuildQueue": "Deny",
+ "StopBuilds": "Allow",
+ "ViewBuildDefinition": "Allow",
+ "EditBuildDefinition": "Deny",
+ "DeleteBuildDefinition": "Deny",
+ "AdministerBuildPermissions": "NotSet"
+ }
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `project_id` - (Required) The ID of the project to assign the permissions.
+* `principal` - (Required) The **group** principal to assign the permissions.
+* `path` - (Required) The folder path to assign the permissions.
+* `replace` - (Optional) Replace (`true`) or merge (`false`) the permissions. Default: `true`.
+* `permissions` - (Required) the permissions to assign. The following permissions are available.
+
+| Permission | Description |
+|--------------------------------|---------------------------------------|
+| ViewBuilds | View builds |
+| EditBuildQuality | Edit build quality |
+| RetainIndefinitely | Retain indefinitely |
+| DeleteBuilds | Delete builds |
+| ManageBuildQualities | Manage build qualities |
+| DestroyBuilds | Destroy builds |
+| UpdateBuildInformation | Update build information |
+| QueueBuilds | Queue builds |
+| ManageBuildQueue | Manage build queue |
+| StopBuilds | Stop builds |
+| ViewBuildDefinition | View build pipeline |
+| EditBuildDefinition | Edit build pipeline |
+| DeleteBuildDefinition | Delete build pipeline |
+| OverrideBuildCheckInValidation | Override check-in validation by build |
+| AdministerBuildPermissions | Administer build permissions |
+
+## Relevant Links
+
+* [Azure DevOps Service REST API 6.0 - Security](https://docs.microsoft.com/en-us/rest/api/azure/devops/security/?view=azure-devops-rest-6.0)
+
+## Import
+
+The resource does not support import.
+
+## PAT Permissions Required
+
+- **Project & Team**: vso.security_manage - Grants the ability to read, write, and manage security permissions.