diff options
| author | xuzhang3 <57888764+xuzhang3@users.noreply.github.com> | 2022-08-05 11:30:51 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-08-05 11:30:51 +0800 |
| commit | 805e11022bab9b05aec296bde266a95b7881f5d5 (patch) | |
| tree | 77b74e91e3444c60cac6431aa2ecba8b4d3ba9db | |
| parent | cef29ccde2435059f650ed87ec7bcabead08ce09 (diff) | |
| parent | 8672e4438179299501642a9654fd22f1c3ea0567 (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.go | 137 | ||||
| -rw-r--r-- | azuredevops/internal/acceptancetests/resource_build_folder_test.go | 35 | ||||
| -rw-r--r-- | azuredevops/internal/acceptancetests/testutils/hcl.go | 16 | ||||
| -rw-r--r-- | azuredevops/internal/service/build/resource_build_folder.go | 184 | ||||
| -rw-r--r-- | azuredevops/internal/service/build/resource_build_folder_test.go | 144 | ||||
| -rw-r--r-- | azuredevops/internal/service/permissions/resource_build_folder_permissions.go | 124 | ||||
| -rw-r--r-- | azuredevops/internal/service/permissions/resource_build_folder_permissions_test.go | 117 | ||||
| -rw-r--r-- | azuredevops/provider.go | 2 | ||||
| -rw-r--r-- | azuredevops/provider_test.go | 2 | ||||
| -rw-r--r-- | main.go | 9 | ||||
| -rw-r--r-- | website/azuredevops.erb | 6 | ||||
| -rw-r--r-- | website/docs/r/build_folder.html.markdown | 49 | ||||
| -rw-r--r-- | website/docs/r/build_folder_permissions.html.markdown | 98 |
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 @@ -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. |
