From d1ea0ff12a7b8d48eb55cab1161f7951932cc8a4 Mon Sep 17 00:00:00 2001 From: Prativa Bawri Date: Tue, 3 Dec 2019 17:42:47 -0800 Subject: [PATCH] Re-designed the resource_configuration schema 1. Re-designed the resource_configuration schema 2. Updated the create and update logic as per the new schema 3. Modify read deployment to read all the resource properties retuned from the API Signed-off-by: Prativa Bawri --- go.mod | 1 + sdk/schema.go | 6 +- sdk/vra7_sdk.go | 2 + utils/utilities.go | 38 +++---- vra7/resource_configuration.go | 166 +++++++++++++++++++++++++++++++ vra7/resource_vra7_deployment.go | 165 ++++++++++-------------------- 6 files changed, 239 insertions(+), 139 deletions(-) create mode 100644 vra7/resource_configuration.go diff --git a/go.mod b/go.mod index 128fcee6..d1bcd39f 100644 --- a/go.mod +++ b/go.mod @@ -14,3 +14,4 @@ require ( gopkg.in/jarcoal/httpmock.v1 v1.0.0-20170412085702-cf52904a3cf0 ) +go 1.13 diff --git a/sdk/schema.go b/sdk/schema.go index 64339707..04966680 100644 --- a/sdk/schema.go +++ b/sdk/schema.go @@ -38,8 +38,8 @@ type BusinessGroup struct { // RequestResourceView - resource view of a provisioned request type RequestResourceView struct { - Content []DeploymentResource `json:"content,omitempty"` - Links []interface{} `json:"links,omitempty"` + Content []interface{} `json:"content,omitempty"` + Links []interface{} `json:"links,omitempty"` } // DeploymentResource - deployment level view of the provisionined request @@ -55,7 +55,7 @@ type DeploymentResource struct { RequestID string `json:"requestId,omitempty"` ResourceID string `json:"resourceId,omitempty"` ResourceType string `json:"resourceType,omitempty"` - ResourcesData DeploymentResourceData `json:"data,omitempty"` + ResourcesData map[string]interface{} `json:"data,omitempty"` } // DeploymentResourceData - view of the resources/machines in a deployment diff --git a/sdk/vra7_sdk.go b/sdk/vra7_sdk.go index 79c3b3ce..e71db660 100644 --- a/sdk/vra7_sdk.go +++ b/sdk/vra7_sdk.go @@ -73,6 +73,8 @@ func (c *APIClient) GetCatalogItemRequestTemplate(catalogItemID string) (*Catalo if unmarshallErr != nil { return nil, unmarshallErr } + // j, _ := json.Marshal(requestTemplate) + // log.Critical("the template is ====> %v ", string(j)) return &requestTemplate, nil } diff --git a/utils/utilities.go b/utils/utilities.go index d9235abc..cd09c9c6 100644 --- a/utils/utilities.go +++ b/utils/utilities.go @@ -5,7 +5,6 @@ import ( "encoding/json" "reflect" "strconv" - "strings" ) // terraform provider constants @@ -53,29 +52,6 @@ func ConvertInterfaceToString(interfaceData interface{}) string { return stringData } -// UpdateResourceConfigurationMap updates the resource configuration with -//the deployment resource data if there is difference -// between the config data and deployment data, return true -func UpdateResourceConfigurationMap( - resourceConfiguration map[string]interface{}, vmData map[string]map[string]interface{}) (map[string]interface{}, bool) { - var changed bool - for configKey1, configValue1 := range resourceConfiguration { - for configKey2, configValue2 := range vmData { - if strings.HasPrefix(configKey1, configKey2+".") { - trimmedKey := strings.TrimPrefix(configKey1, configKey2+".") - currentValue := configValue1 - updatedValue := ConvertInterfaceToString(configValue2[trimmedKey]) - - if updatedValue != "" && updatedValue != currentValue { - resourceConfiguration[configKey1] = updatedValue - changed = true - } - } - } - } - return resourceConfiguration, changed -} - // ReplaceValueInRequestTemplate replaces the value for a given key in a catalog // request template. func ReplaceValueInRequestTemplate(templateInterface map[string]interface{}, field string, value interface{}) (map[string]interface{}, bool) { @@ -116,3 +92,17 @@ func AddValueToRequestTemplate(templateInterface map[string]interface{}, field s //Return updated map interface type return templateInterface } + +// ResourceMapper returns the mapping of resource attributes from ResourceView APIs +// to Catalog Item Request Template APIs +func ResourceMapper() map[string]string { + m := make(map[string]string) + m["MachineName"] = "name" + m["MachineDescription"] = "description" + m["MachineMemory"] = "memory" + m["MachineStorage"] = "storage" + m["MachineCPU"] = "cpu" + m["MachineStatus"] = "status" + m["MachineType"] = "type" + return m +} diff --git a/vra7/resource_configuration.go b/vra7/resource_configuration.go new file mode 100644 index 00000000..1924e773 --- /dev/null +++ b/vra7/resource_configuration.go @@ -0,0 +1,166 @@ +package vra7 + +import ( + "reflect" + "strconv" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/terraform-providers/terraform-provider-vra7/utils" +) + +// ResourceConfigurationStruct - structure representing the resource_configuration +type ResourceConfigurationStruct struct { + Name string `json:"name"` + Configuration map[string]interface{} `json:"configuration"` +} + +func resourceConfigurationSchema() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "configuration": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + } +} + +func expandResourceConfiguration(rConfigurations []interface{}) []ResourceConfigurationStruct { + configs := make([]ResourceConfigurationStruct, 0, len(rConfigurations)) + + for _, config := range rConfigurations { + configMap := config.(map[string]interface{}) + + rConfig := ResourceConfigurationStruct{ + Name: configMap["name"].(string), + Configuration: configMap["configuration"].(map[string]interface{}), + } + configs = append(configs, rConfig) + } + return configs +} + +func flattenResourceConfigurations(configs []ResourceConfigurationStruct) []map[string]interface{} { + if len(configs) == 0 { + return make([]map[string]interface{}, 0) + } + rConfigs := make([]map[string]interface{}, 0, len(configs)) + for _, config := range configs { + componentName, resourceDataMap := parseDataMap(config.Configuration) + helper := make(map[string]interface{}) + helper["name"] = componentName + helper["configuration"] = resourceDataMap + rConfigs = append(rConfigs, helper) + } + return rConfigs +} + +func parseDataMap(resourceData map[string]interface{}) (string, map[string]interface{}) { + m := make(map[string]interface{}) + componentName := "" + resourcePropertyMapper := utils.ResourceMapper() + for key, value := range resourceData { + + // Component property is within data of a resource, so fetching it from there and putting it as resource level property + if key == "Component" { + componentName = convToString(value) + } + if i, ok := resourcePropertyMapper[key]; ok { + key = i + } + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.Slice: + parseArray(key, m, value.([]interface{})) + case reflect.Map: + parseMap(key, m, value.(map[string]interface{})) + default: + m[key] = convToString(value) + } + } + return componentName, m +} + +func parseMap(prefix string, m map[string]interface{}, data map[string]interface{}) { + + for key, value := range data { + v := reflect.ValueOf(value) + + switch v.Kind() { + case reflect.Slice: + parseArray(prefix+"."+key, m, value.([]interface{})) + case reflect.Map: + parseMap(prefix+"."+key, m, value.(map[string]interface{})) + default: + m[prefix+"."+key] = convToString(value) + } + } +} + +func parseArray(prefix string, m map[string]interface{}, value []interface{}) { + + for index, val := range value { + v := reflect.ValueOf(val) + switch v.Kind() { + case reflect.Map: + /* for properties like NETWORK_LIST, DISK_VOLUMES etc, the value is a slice of map as follows. + Out of all the information, only data is important information, so leaving out rest of the properties + "NETWORK_LIST":[ + { + "componentTypeId":"", + "componentId":null, + "classId":"", + "typeFilter":null, + "data":{ + "NETWORK_MAC_ADDRESS":"00:50:56:b6:78:c6", + "NETWORK_NAME":"dvPortGroup-wdc-sdm-vm-1521" + } + } + ] + */ + objMap := val.(map[string]interface{}) + for k, v := range objMap { + if k == "data" { + parseMap(prefix+"."+convToString(index), m, v.(map[string]interface{})) + } + } + default: + m[prefix+"."+convToString(index)] = convToString(val) + } + } +} + +func convToString(value interface{}) string { + + v := reflect.ValueOf(value) + switch v.Kind() { + case reflect.String: + return value.(string) + case reflect.Float64: + return strconv.FormatFloat(value.(float64), 'f', 0, 64) + case reflect.Float32: + return strconv.FormatFloat(value.(float64), 'f', 0, 32) + case reflect.Int: + return strconv.Itoa(value.(int)) + case reflect.Int32: + return strconv.Itoa(value.(int)) + case reflect.Int64: + return strconv.FormatInt(value.(int64), 10) + case reflect.Bool: + return strconv.FormatBool(value.(bool)) + } + return "" +} diff --git a/vra7/resource_vra7_deployment.go b/vra7/resource_vra7_deployment.go index 2c591788..f564726d 100644 --- a/vra7/resource_vra7_deployment.go +++ b/vra7/resource_vra7_deployment.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" "sort" - "strconv" "strings" "time" @@ -37,9 +36,9 @@ type ProviderSchema struct { BusinessGroupName string WaitTimeout int RequestStatus string - FailedMessage string DeploymentConfiguration map[string]interface{} - ResourceConfiguration map[string]interface{} + //ResourceConfiguration map[string]interface{} + ResourceConfiguration []ResourceConfigurationStruct } func resourceVra7Deployment() *schema.Resource { @@ -48,6 +47,9 @@ func resourceVra7Deployment() *schema.Resource { Read: resourceVra7DeploymentRead, Update: resourceVra7DeploymentUpdate, Delete: resourceVra7DeploymentDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, Schema: map[string]*schema.Schema{ "catalog_item_name": { @@ -89,23 +91,12 @@ func resourceVra7Deployment() *schema.Resource { Computed: true, ForceNew: true, }, - "failed_message": { - Type: schema.TypeString, - Computed: true, - ForceNew: true, - Optional: true, - }, "deployment_configuration": { Type: schema.TypeMap, Optional: true, Elem: schema.TypeString, }, - "resource_configuration": { - Type: schema.TypeMap, - Optional: true, - Computed: true, - Elem: schema.TypeString, - }, + "resource_configuration": resourceConfigurationSchema(), }, } } @@ -127,6 +118,7 @@ func (s byLength) Swap(i, j int) { // This function creates a new vRA 7 Deployment using configuration in a user's Terraform file. // The Deployment is produced by invoking a catalog item that is specified in the configuration. func resourceVra7DeploymentCreate(d *schema.ResourceData, meta interface{}) error { + log.Info("Creating the resource vra7_deployment...") vraClient = meta.(*sdk.APIClient) // Get client handle p := readProviderConfiguration(d) @@ -156,31 +148,20 @@ func resourceVra7DeploymentCreate(d *schema.ResourceData, meta interface{}) erro } log.Info("createResource->key_list %v\n", componentNameList) - // Arrange component names in descending order of text length. - // Component names are sorted this way because '.', which is used as a separator, may also occur within - // component names. In these situations, the longest name match that includes '.'s should win. sort.Sort(byLength(componentNameList)) //Update request template field values with values from user configuration. - for configKey, configValue := range p.ResourceConfiguration { + for _, rConfig := range p.ResourceConfiguration { for _, componentName := range componentNameList { - // User-supplied resource configuration keys are expected to be of the form: - // .. // Extract the property names and values for each component in the blueprint, and add/update // them in the right location in the request template. - if strings.HasPrefix(configKey, componentName) { - propertyName := strings.TrimPrefix(configKey, componentName+".") - if len(propertyName) == 0 { - return fmt.Errorf( - "resource_configuration key is not in correct format. Expected %s to start with %s", - configKey, componentName+".") + if rConfig.Name == componentName { + for propertyName, propertyValue := range rConfig.Configuration { + requestTemplate.Data[componentName] = updateRequestTemplate( + requestTemplate.Data[componentName].(map[string]interface{}), + propertyName, + propertyValue) } - // Function call which changes request template field values with user-supplied values - requestTemplate.Data[componentName] = updateRequestTemplate( - requestTemplate.Data[componentName].(map[string]interface{}), - propertyName, - configValue) - break } } } @@ -199,6 +180,7 @@ func resourceVra7DeploymentCreate(d *schema.ResourceData, meta interface{}) erro if err != nil { return err } + log.Info("Finished creating the resource vra7_deployment with request id %s", d.Id()) return resourceVra7DeploymentRead(d, meta) } @@ -214,8 +196,10 @@ func updateRequestTemplate(templateInterface map[string]interface{}, field strin // Terraform call - terraform apply // This function updates the state of a vRA 7 Deployment when changes to a Terraform file are applied. -// The update is performed on the Deployment using supported (day-2) actions. +//The update is performed on the Deployment using supported (day-2) actions. func resourceVra7DeploymentUpdate(d *schema.ResourceData, meta interface{}) error { + + log.Info("Updating the resource vra7_deployment with request id %s", d.Id()) vraClient = meta.(*sdk.APIClient) // Get the ID of the catalog request that was used to provision this Deployment. catalogItemRequestID := d.Id() @@ -274,26 +258,20 @@ func resourceVra7DeploymentUpdate(d *schema.ResourceData, meta interface{}) erro return fmt.Errorf("Error retrieving reconfigure action template for the component %v: %v ", componentName, err.Error()) } configChanged := false - for configKey := range p.ResourceConfiguration { + + for _, rConfig := range p.ResourceConfiguration { var returnFlag bool //compare resource list (resource_name) with user configuration fields - if strings.HasPrefix(configKey, componentName+".") { - //If user_configuration contains resource_list element - // then split user configuration key into resource_name and field_name - nameList := strings.Split(configKey, componentName+".") - //actionResponseInterface := actionResponse.(map[string]interface{}) - //Function call which changes the template field values with user values - //Replace existing values with new values in resource child template - resourceActionTemplate.Data, returnFlag = utils.ReplaceValueInRequestTemplate( - resourceActionTemplate.Data, - nameList[1], - p.ResourceConfiguration[configKey]) - if returnFlag { - configChanged = true + if rConfig.Name == componentName { + for propertyName, propertyValue := range rConfig.Configuration { + resourceActionTemplate.Data, returnFlag = utils.ReplaceValueInRequestTemplate( + resourceActionTemplate.Data, propertyName, propertyValue) + if returnFlag { + configChanged = true + } } } } - oldData, _ := d.GetChange("resource_configuration") // If template value got changed then set post call and update resource child if configChanged { // This request id is for the reconfigure action on this machine and @@ -301,22 +279,12 @@ func resourceVra7DeploymentUpdate(d *schema.ResourceData, meta interface{}) erro // It will not replace the initial catalog item request id requestID, err := vraClient.PostResourceAction(resources.ID, reconfigureActionID, resourceActionTemplate) if err != nil { - err = d.Set("resource_configuration", oldData) - if err != nil { - return err - } log.Errorf("The update request failed with error: %v ", err) return err } - status, err := waitForRequestCompletion(d, meta, requestID) + _, err = waitForRequestCompletion(d, meta, requestID) if err != nil { - // if the update request fails, go back to the old state and return the error - if status == sdk.Failed { - setErr := d.Set("resource_configuration", oldData) - if setErr != nil { - return setErr - } - } + log.Errorf("The update request failed with error: %v ", err) return err } } @@ -325,6 +293,7 @@ func resourceVra7DeploymentUpdate(d *schema.ResourceData, meta interface{}) erro } } } + log.Info("Finished updating the resource vra7_deployment with request id %s", d.Id()) return resourceVra7DeploymentRead(d, meta) } @@ -332,6 +301,7 @@ func resourceVra7DeploymentUpdate(d *schema.ResourceData, meta interface{}) erro // This function retrieves the latest state of a vRA 7 deployment. Terraform updates its state based on // the information returned by this function. func resourceVra7DeploymentRead(d *schema.ResourceData, meta interface{}) error { + log.Info("Reading the resource vra7_deployment with request id %s ", d.Id()) vraClient = meta.(*sdk.APIClient) // Get the ID of the catalog request that was used to provision this Deployment. This id // will remain the same for this deployment across any actions on the machines like reconfigure, etc. @@ -347,52 +317,30 @@ func resourceVra7DeploymentRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Resource view failed to load: %v", errTemplate) } - resourceDataMap := make(map[string]map[string]interface{}) + var resourceConfigList []ResourceConfigurationStruct for _, resource := range requestResourceView.Content { - if resource.ResourceType == sdk.InfrastructureVirtual { - resourceData := resource.ResourcesData - log.Info("The resource data map of the resource %v is: \n%v", resourceData.Component, resource.ResourcesData) - dataVals := make(map[string]interface{}) - resourceDataMap[resourceData.Component] = dataVals - dataVals[sdk.MachineCPU] = resourceData.CPU - dataVals[sdk.MachineStorage] = resourceData.Storage - dataVals[sdk.IPAddress] = resourceData.IPAddress - dataVals[sdk.MachineMemory] = resourceData.Memory - dataVals[sdk.MachineName] = resourceData.MachineName - dataVals[sdk.MachineGuestOs] = resourceData.MachineGuestOperatingSystem - dataVals[sdk.MachineBpName] = resourceData.MachineBlueprintName - dataVals[sdk.MachineType] = resourceData.MachineType - dataVals[sdk.MachineReservationName] = resourceData.MachineReservationName - dataVals[sdk.MachineInterfaceType] = resourceData.MachineInterfaceType - dataVals[sdk.MachineID] = resourceData.MachineID - dataVals[sdk.MachineGroupName] = resourceData.MachineGroupName - dataVals[sdk.MachineDestructionDate] = resourceData.MachineDestructionDate - // Handle Network Info - for idx, netDetails := range resourceData.Networks { - log.Info("The Network list value is for idx %i = %+v", idx, netDetails) - networkIndexName := "Network" + strconv.Itoa(idx) - dataVals[networkIndexName+".IPAddress"] = netDetails.NetworkAddressInfo.IPAddress - dataVals[networkIndexName+".MACAddress"] = netDetails.NetworkAddressInfo.MACAddress - dataVals[networkIndexName+".Name"] = netDetails.NetworkAddressInfo.Name + rMap := resource.(map[string]interface{}) + for key, value := range rMap { + if rMap["resourceType"] == "Infrastructure.Virtual" { + if key == "data" { + var resourceConfigStruct ResourceConfigurationStruct + resourceConfigStruct.Configuration = value.(map[string]interface{}) + resourceConfigList = append(resourceConfigList, resourceConfigStruct) + } } - } } - resourceConfiguration, _ := d.Get("resource_configuration").(map[string]interface{}) - resourceConfiguration, changed := utils.UpdateResourceConfigurationMap(resourceConfiguration, resourceDataMap) - - if changed { - setError := d.Set("resource_configuration", resourceConfiguration) - if setError != nil { - return fmt.Errorf(setError.Error()) - } + if err := d.Set("resource_configuration", flattenResourceConfigurations(resourceConfigList)); err != nil { + return fmt.Errorf("error setting resource configuration - error: %v", err) } + log.Info("Finished reading the resource vra7_deployment with request id %s", d.Id()) return nil } //Function use - To delete resources which are created by terraform and present in state file //Terraform call - terraform destroy func resourceVra7DeploymentDelete(d *schema.ResourceData, meta interface{}) error { + log.Info("Deleting the resource vra7_deployment with request id %s", d.Id()) vraClient = meta.(*sdk.APIClient) //Get requester machine ID from schema.dataresource catalogItemRequestID := d.Id() @@ -452,6 +400,7 @@ func resourceVra7DeploymentDelete(d *schema.ResourceData, meta interface{}) erro } } } + log.Info("Finished deleting the resource vra7_deployment....") return nil } @@ -473,20 +422,13 @@ func (p *ProviderSchema) checkResourceConfigValidity(requestTemplate *sdk.Catalo // if the key in config is machine1.vsphere.custom.location, match every string after each dot // until a matching string is found in componentSet. // If found, it's a valid key else the component name is invalid - for k := range p.ResourceConfiguration { - var key = k - var isValid bool - for strings.LastIndex(key, ".") != -1 { - lastIndex := strings.LastIndex(key, ".") - key = key[0:lastIndex] - if _, ok := componentSet[key]; ok { - log.Info("The component name %s in the terraform config file is valid ", key) - isValid = true - break - } - } - if !isValid { - invalidKeys = append(invalidKeys, k) + for _, k := range p.ResourceConfiguration { + var key = k.Name + if _, ok := componentSet[key]; ok { + log.Info("The component name %s in the terraform config file is valid ", key) + continue + } else { + invalidKeys = append(invalidKeys, key) } } // there are invalid resource config keys in the terraform config file, abort and throw an error @@ -617,8 +559,7 @@ func readProviderConfiguration(d *schema.ResourceData) *ProviderSchema { BusinessGroupName: strings.TrimSpace(d.Get("businessgroup_name").(string)), BusinessGroupID: strings.TrimSpace(d.Get("businessgroup_id").(string)), WaitTimeout: d.Get("wait_timeout").(int) * 60, - FailedMessage: strings.TrimSpace(d.Get("failed_message").(string)), - ResourceConfiguration: d.Get("resource_configuration").(map[string]interface{}), + ResourceConfiguration: expandResourceConfiguration(d.Get("resource_configuration").(*schema.Set).List()), DeploymentConfiguration: d.Get("deployment_configuration").(map[string]interface{}), }