Skip to content

Commit

Permalink
Implements resource for AzDO Variable Group support (microsoft#195)
Browse files Browse the repository at this point in the history
* Add taskagent client to config.go.

* Fix task agent client field name in aggregated client type.

* added mock client for taskagent

* Add variable group resource and example.

* Register variable group in provider.

* Change variable group resource to use TypeSet instead of TypeMap.

* Added a SchemaSetFunc to determine hash for item key.

* Move parse project ID and resource ID helper function to tfhelpers file (originally inside build definition).

* Update example main.tf with variable group description.

* Implement variable group resource create and read.

* Implement variable group resource update and delete.

* Added acceptance test for var groups

* Add variable group mock data for unit testing and unit test for expand/flatten.

* Fixed nil pointer response from SDK

* Lint check fix

* Added docs

* Add issue for Variable Group allow_access feature inline.

* Updated docs in response to comments

* Resolving comments -- adding validation and removing unneeded comments


Co-authored-by: Kevin Hartman <[email protected]>
  • Loading branch information
kevinhartman authored and awkwardindustries committed Nov 7, 2019
1 parent 25d1ab6 commit fb2d779
Show file tree
Hide file tree
Showing 11 changed files with 1,195 additions and 24 deletions.
688 changes: 688 additions & 0 deletions azdosdkmocks/taskagent_sdk_mock.go

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions azuredevops/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/microsoft/azure-devops-go-api/azuredevops/memberentitlementmanagement"
"github.com/microsoft/azure-devops-go-api/azuredevops/operations"
"github.com/microsoft/azure-devops-go-api/azuredevops/serviceendpoint"
"github.com/microsoft/azure-devops-go-api/azuredevops/taskagent"
)

// Aggregates all of the underlying clients into a single data
Expand All @@ -29,6 +30,7 @@ type aggregatedClient struct {
GraphClient graph.Client
OperationsClient operations.Client
ServiceEndpointClient serviceendpoint.Client
TaskAgentClient taskagent.Client
MemberEntitleManagementClient memberentitlementmanagement.Client
ctx context.Context
}
Expand Down Expand Up @@ -74,6 +76,13 @@ func getAzdoClient(azdoPAT string, organizationURL string) (*aggregatedClient, e
return nil, err
}

// client for these APIs (includes CRUD for AzDO variable groups):
taskagentClient, err := taskagent.NewClient(ctx, connection)
if err != nil {
log.Printf("getAzdoClient(): taskagent.NewClient failed.")
return nil, err
}

// client for these APIs:
// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/?view=azure-devops-rest-5.1
gitReposClient, err := git.NewClient(ctx, connection)
Expand Down Expand Up @@ -102,6 +111,7 @@ func getAzdoClient(azdoPAT string, organizationURL string) (*aggregatedClient, e
GraphClient: graphClient,
OperationsClient: operationsClient,
ServiceEndpointClient: serviceEndpointClient,
TaskAgentClient: taskagentClient,
MemberEntitleManagementClient: memberentitlementmanagementClient,
ctx: ctx,
}
Expand Down
1 change: 1 addition & 0 deletions azuredevops/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func Provider() *schema.Provider {
"azuredevops_build_definition": resourceBuildDefinition(),
"azuredevops_project": resourceProject(),
"azuredevops_serviceendpoint": resourceServiceEndpoint(),
"azuredevops_variable_group": resourceVariableGroup(),
"azuredevops_azure_git_repository": resourceAzureGitRepository(),
"azuredevops_user_entitlement": resourceUserEntitlement(),
"azuredevops_group_membership": resourceGroupMembership(),
Expand Down
1 change: 1 addition & 0 deletions azuredevops/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestAzureDevOpsProvider_HasChildResources(t *testing.T) {
"azuredevops_build_definition",
"azuredevops_project",
"azuredevops_serviceendpoint",
"azuredevops_variable_group",
"azuredevops_azure_git_repository",
"azuredevops_user_entitlement",
"azuredevops_group_membership",
Expand Down
12 changes: 3 additions & 9 deletions azuredevops/resource_build_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/converter"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/tfhelper"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
Expand Down Expand Up @@ -144,7 +145,7 @@ func createBuildDefinition(clients *aggregatedClient, buildDefinition *build.Bui

func resourceBuildDefinitionRead(d *schema.ResourceData, m interface{}) error {
clients := m.(*aggregatedClient)
projectID, buildDefinitionID, err := parseIdentifiers(d)
projectID, buildDefinitionID, err := tfhelper.ParseProjectIdAndResourceId(d)

if err != nil {
return err
Expand All @@ -169,7 +170,7 @@ func resourceBuildDefinitionDelete(d *schema.ResourceData, m interface{}) error
}

clients := m.(*aggregatedClient)
projectID, buildDefinitionID, err := parseIdentifiers(d)
projectID, buildDefinitionID, err := tfhelper.ParseProjectIdAndResourceId(d)
if err != nil {
return err
}
Expand Down Expand Up @@ -203,13 +204,6 @@ func resourceBuildDefinitionUpdate(d *schema.ResourceData, m interface{}) error
return nil
}

func parseIdentifiers(d *schema.ResourceData) (string, int, error) {
projectID := d.Get("project_id").(string)
buildDefinitionID, err := strconv.Atoi(d.Id())

return projectID, buildDefinitionID, err
}

func flattenRepository(buildDefiniton *build.BuildDefinition) interface{} {
yamlFilePath := ""

Expand Down
223 changes: 223 additions & 0 deletions azuredevops/resource_variable_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package azuredevops

import (
"fmt"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/microsoft/azure-devops-go-api/azuredevops/taskagent"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/converter"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/tfhelper"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/utils/validate"
)

func resourceVariableGroup() *schema.Resource {
return &schema.Resource{
Create: resourceVariableGroupCreate,
Read: resourceVariableGroupRead,
Update: resourceVariableGroupUpdate,
Delete: resourceVariableGroupDelete,

Schema: map[string]*schema.Schema{
"project_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.UUID,
},
"name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validate.NoEmptyStrings,
},
"description": {
Type: schema.TypeString,
Optional: true,
Default: "",
},
// Not supported by API: https://github.com/microsoft/terraform-provider-azuredevops/issues/200
// "allow_access": {
// Type: schema.TypeBool,
// Required: true,
// },
"variable": {
Type: schema.TypeSet,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"value": {
Type: schema.TypeString,
Optional: true,
Default: "",
},
"is_secret": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
},
Required: true,
MinItems: 1,
Set: func(i interface{}) int {
item := i.(map[string]interface{})
return schema.HashString(item["name"].(string))
},
},
},
}
}

func resourceVariableGroupCreate(d *schema.ResourceData, m interface{}) error {
clients := m.(*aggregatedClient)
variableGroupParameters, projectID := expandVariableGroupParameters(d)

addedVariableGroup, err := createVariableGroup(clients, variableGroupParameters, projectID)
if err != nil {
return fmt.Errorf("Error creating variable group in Azure DevOps: %+v", err)
}

flattenVariableGroup(d, addedVariableGroup, projectID)
return nil
}

func resourceVariableGroupRead(d *schema.ResourceData, m interface{}) error {
clients := m.(*aggregatedClient)

projectID, variableGroupID, err := tfhelper.ParseProjectIdAndResourceId(d)
if err != nil {
return fmt.Errorf("Error parsing the variable group ID from the Terraform resource data: %v", err)
}

variableGroup, err := clients.TaskAgentClient.GetVariableGroup(
clients.ctx,
taskagent.GetVariableGroupArgs{
GroupId: &variableGroupID,
Project: &projectID,
},
)
if err != nil {
return fmt.Errorf("Error looking up variable group given ID (%v) and project ID (%v): %v", variableGroupID, projectID, err)
}

flattenVariableGroup(d, variableGroup, &projectID)
return nil
}

func resourceVariableGroupUpdate(d *schema.ResourceData, m interface{}) error {
clients := m.(*aggregatedClient)
variableGroupParams, projectID := expandVariableGroupParameters(d)

_, variableGroupID, err := tfhelper.ParseProjectIdAndResourceId(d)
if err != nil {
return fmt.Errorf("Error parsing the variable group ID from the Terraform resource data: %v", err)
}

updatedVariableGroup, err := updateVariableGroup(clients, variableGroupParams, &variableGroupID, projectID)
if err != nil {
return fmt.Errorf("Error updating variable group in Azure DevOps: %+v", err)
}

flattenVariableGroup(d, updatedVariableGroup, projectID)
return nil
}

func resourceVariableGroupDelete(d *schema.ResourceData, m interface{}) error {
clients := m.(*aggregatedClient)
projectID, variableGroupID, err := tfhelper.ParseProjectIdAndResourceId(d)
if err != nil {
return fmt.Errorf("Error parsing the variable group ID from the Terraform resource data: %v", err)
}

return deleteVariableGroup(clients, &projectID, &variableGroupID)
}

// Make the Azure DevOps API call to create the variable group
func createVariableGroup(clients *aggregatedClient, variableGroupParams *taskagent.VariableGroupParameters, project *string) (*taskagent.VariableGroup, error) {
createdVariableGroup, err := clients.TaskAgentClient.AddVariableGroup(
clients.ctx,
taskagent.AddVariableGroupArgs{
Group: variableGroupParams,
Project: project,
})

return createdVariableGroup, err
}

// Make the Azure DevOps API call to update the variable group
func updateVariableGroup(clients *aggregatedClient, parameters *taskagent.VariableGroupParameters, variableGroupID *int, project *string) (*taskagent.VariableGroup, error) {
updatedVariableGroup, err := clients.TaskAgentClient.UpdateVariableGroup(
clients.ctx,
taskagent.UpdateVariableGroupArgs{
Project: project,
GroupId: variableGroupID,
Group: parameters,
})

return updatedVariableGroup, err
}

// Make the Azure DevOps API call to delete the variable group
func deleteVariableGroup(clients *aggregatedClient, project *string, variableGroupID *int) error {
err := clients.TaskAgentClient.DeleteVariableGroup(
clients.ctx,
taskagent.DeleteVariableGroupArgs{
Project: project,
GroupId: variableGroupID,
})

return err
}

// Convert internal Terraform data structure to an AzDO data structure
func expandVariableGroupParameters(d *schema.ResourceData) (*taskagent.VariableGroupParameters, *string) {
projectID := converter.String(d.Get("project_id").(string))
variables := d.Get("variable").(*schema.Set).List()

variableMap := make(map[string]taskagent.VariableValue)

for _, variable := range variables {
asMap := variable.(map[string]interface{})
variableMap[asMap["name"].(string)] = taskagent.VariableValue{
Value: converter.String(asMap["value"].(string)),
IsSecret: converter.Bool(asMap["is_secret"].(bool)),
}
}

variableGroup := &taskagent.VariableGroupParameters{
Name: converter.String(d.Get("name").(string)),
Description: converter.String(d.Get("description").(string)),
Variables: &variableMap,
}

return variableGroup, projectID
}

// Convert AzDO data structure to internal Terraform data structure
func flattenVariableGroup(d *schema.ResourceData, variableGroup *taskagent.VariableGroup, projectID *string) {
d.SetId(fmt.Sprintf("%d", *variableGroup.Id))
d.Set("name", *variableGroup.Name)
d.Set("description", *variableGroup.Description)
d.Set("variable", flattenVariables(variableGroup))
d.Set("project_id", projectID)
}

// Convert AzDO Variables data structure to Terraform TypeSet
func flattenVariables(variableGroup *taskagent.VariableGroup) interface{} {
// Preallocate list of variable prop maps
variables := make([]map[string]interface{}, len(*variableGroup.Variables))

index := 0
for k, v := range *variableGroup.Variables {
variables[index] = map[string]interface{}{
"name": k,
"value": converter.ToString(v.Value, ""),
"is_secret": converter.ToBool(v.IsSecret, false),
}
index = index + 1
}

return variables
}
Loading

0 comments on commit fb2d779

Please sign in to comment.