From d973230fafe9168b7bebd27e1c0307faeaa4891d 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 --- sdk/schema.go | 69 ++--- utils/utilities.go | 26 +- utils/utilities_test.go | 19 +- vra7/resource_configuration.go | 265 +++++++++++++++++++ vra7/resource_vra7_deployment.go | 368 ++++++++++++-------------- vra7/resource_vra7_deployment_test.go | 4 +- 6 files changed, 486 insertions(+), 265 deletions(-) create mode 100644 vra7/resource_configuration.go diff --git a/sdk/schema.go b/sdk/schema.go index 64339707..3fcab30f 100644 --- a/sdk/schema.go +++ b/sdk/schema.go @@ -38,56 +38,33 @@ 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 type DeploymentResource struct { - RequestState string `json:"requestState,omitempty"` - Description string `json:"description,omitempty"` - LastUpdated string `json:"lastUpdated,omitempty"` - TenantID string `json:"tenantId,omitempty"` - Name string `json:"name,omitempty"` - BusinessGroupID string `json:"businessGroupId,omitempty"` - DateCreated string `json:"dateCreated,omitempty"` - Status string `json:"status,omitempty"` - RequestID string `json:"requestId,omitempty"` - ResourceID string `json:"resourceId,omitempty"` - ResourceType string `json:"resourceType,omitempty"` - ResourcesData DeploymentResourceData `json:"data,omitempty"` -} - -// DeploymentResourceData - view of the resources/machines in a deployment -type DeploymentResourceData struct { - Memory int `json:"MachineMemory,omitempty"` - CPU int `json:"MachineCPU,omitempty"` - IPAddress string `json:"ip_address,omitempty"` - Storage int `json:"MachineStorage,omitempty"` - MachineInterfaceType string `json:"MachineInterfaceType,omitempty"` - MachineName string `json:"MachineName,omitempty"` - MachineGuestOperatingSystem string `json:"MachineGuestOperatingSystem,omitempty"` - MachineDestructionDate string `json:"MachineDestructionDate,omitempty"` - MachineGroupName string `json:"MachineGroupName,omitempty"` - MachineBlueprintName string `json:"MachineBlueprintName,omitempty"` - MachineReservationName string `json:"MachineReservationName,omitempty"` - MachineType string `json:"MachineType,omitempty"` - MachineID string `json:"machineId,omitempty"` - MachineExpirationDate string `json:"MachineExpirationDate,omitempty"` - Component string `json:"Component,omitempty"` - Expire bool `json:"Expire,omitempty"` - Reconfigure bool `json:"Reconfigure,omitempty"` - Reset bool `json:"Reset,omitempty"` - Reboot bool `json:"Reboot,omitempty"` - PowerOff bool `json:"PowerOff,omitempty"` - Destroy bool `json:"Destroy,omitempty"` - Shutdown bool `json:"Shutdown,omitempty"` - Suspend bool `json:"Suspend,omitempty"` - Reprovision bool `json:"Reprovision,omitempty"` - ChangeLease bool `json:"ChangeLease,omitempty"` - ChangeOwner bool `json:"ChangeOwner,omitempty"` - CreateSnapshot bool `json:"CreateSnapshot,omitempty"` - Networks []NWDetails `json:"NETWORK_LIST,omitempty"` + ResourceID string `json:"resourceId,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Status string `json:"status,omitempty"` + CatalogItemID string `json:"catalogItemId,omitempty"` + CatalogItemLabel string `json:"catalogItemLabel,omitempty"` + RequestID string `json:"requestId,omitempty"` + RequestState string `json:"requestState,omitempty"` + ResourceType string `json:"resourceType,omitempty"` + Owners []string `json:"owners,omitempty"` + BusinessGroupID string `json:"businessGroupId,omitempty"` + TenantID string `json:"tenantId,omitempty"` + DateCreated string `json:"dateCreated,omitempty"` + LastUpdated string `json:"lastUpdated,omitempty"` + Lease struct { + Start string `json:"start,omitempty"` + End string `json:"end,omitempty"` + } `json:"lease,omitempty"` + + ParentResourceID string `json:"parentResourceId,omitempty"` + ResourcesData map[string]interface{} `json:"data,omitempty"` } //Networks Related structs diff --git a/utils/utilities.go b/utils/utilities.go index 13861b0a..25a9286a 100644 --- a/utils/utilities.go +++ b/utils/utilities.go @@ -3,16 +3,16 @@ package utils import ( "bytes" "encoding/json" - "github.com/op/go-logging" "reflect" "strconv" "strings" + + "github.com/op/go-logging" ) // terraform provider constants const ( // utility constants - LoggerID = "terraform-provider-vra7" ) @@ -58,8 +58,8 @@ func ConvertInterfaceToString(interfaceData interface{}) string { return stringData } -// Parse value and if it's JSON string, unmarshal it -func UnmarshalJsonStringIfNecessary(field string, value interface{}) interface{} { +// UnmarshalJSONStringIfNecessary parses value and if it's JSON string, unmarshal it +func UnmarshalJSONStringIfNecessary(field string, value interface{}) interface{} { // Cast value to string. Provider schema requires DeploymentConfiguration to be map[string]string stringValue, ok := value.(string) @@ -117,7 +117,7 @@ func ReplaceValueInRequestTemplate(templateInterface map[string]interface{}, fie } else if key == field { //If value type is not map then compare field name with provided field name //If both matches then update field value with provided value - templateInterface[key] = value + templateInterface[key] = UnmarshalJSONStringIfNecessary(field, value) return templateInterface, true } } @@ -135,9 +135,23 @@ func AddValueToRequestTemplate(templateInterface map[string]interface{}, field s template, _ := v.(map[string]interface{}) v = AddValueToRequestTemplate(template, field, value) } else { //if i == "data" { - templateInterface[field] = value + templateInterface[field] = UnmarshalJSONStringIfNecessary(field, value) } } //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/utils/utilities_test.go b/utils/utilities_test.go index d468fba0..f3f35e3e 100644 --- a/utils/utilities_test.go +++ b/utils/utilities_test.go @@ -1,8 +1,9 @@ package utils import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestUnmarshalJsonStringIfNecessaryFunction(t *testing.T) { @@ -13,15 +14,15 @@ func TestUnmarshalJsonStringIfNecessaryFunction(t *testing.T) { "key1": "string", "key2": 1, }} - computedValue1 := UnmarshalJsonStringIfNecessary(fieldName, notStringValue) + computedValue1 := UnmarshalJSONStringIfNecessary(fieldName, notStringValue) assertLocal.Equal(notStringValue, computedValue1) - var notJsonStringValue = "some custom value" - computedValue2 := UnmarshalJsonStringIfNecessary(fieldName, notJsonStringValue) - assertLocal.Equal(notJsonStringValue, computedValue2) + var notJSONStringValue = "some custom value" + computedValue2 := UnmarshalJSONStringIfNecessary(fieldName, notJSONStringValue) + assertLocal.Equal(notJSONStringValue, computedValue2) - var jsonStringValue = "[\"bg1\", \"bg2\"]" - var expectedJsonValue = []interface{}{"bg1", "bg2"} - computedValue3 := UnmarshalJsonStringIfNecessary(fieldName, jsonStringValue) - assertLocal.Equal(expectedJsonValue, computedValue3) + var jSONStringValue = "[\"bg1\", \"bg2\"]" + var expectedJSONValue = []interface{}{"bg1", "bg2"} + computedValue3 := UnmarshalJSONStringIfNecessary(fieldName, jSONStringValue) + assertLocal.Equal(expectedJSONValue, computedValue3) } diff --git a/vra7/resource_configuration.go b/vra7/resource_configuration.go new file mode 100644 index 00000000..4a1cf6f6 --- /dev/null +++ b/vra7/resource_configuration.go @@ -0,0 +1,265 @@ +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 { + ComponentName string `json:"component_name,omitempty"` + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + ResourceID string `json:"resource_id,omitempty"` + Status string `json:"status,omitempty"` + RequestID string `json:"request_id,omitempty"` + RequestState string `json:"request_state,omitempty"` + ResourceType string `json:"resource_type,omitempty"` + Configuration map[string]interface{} `json:"configuration,omitempty"` + Owners []interface{} `json:"owners,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + DateCreated string `json:"last_created,omitempty"` + LastUpdated string `json:"last_updated,omitempty"` + Lease struct { + Start string `json:"start,omitempty"` + End string `json:"end,omitempty"` + } `json:"lease,omitempty"` + ParentResourceID string `json:"parent_resource_id,omitempty"` +} + +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, + }, + }, + "component_name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "resource_id": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "request_id": { + Type: schema.TypeString, + Computed: true, + }, + "request_state": { + Type: schema.TypeString, + Computed: true, + }, + "resource_type": { + Type: schema.TypeString, + Computed: true, + }, + "owners": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "tenant_id": { + Type: schema.TypeString, + Computed: true, + }, + "date_created": { + Type: schema.TypeString, + Computed: true, + }, + "last_updated": { + Type: schema.TypeString, + Computed: true, + }, + "lease_start_date": { + Type: schema.TypeString, + Computed: true, + }, + "lease_end_date": { + Type: schema.TypeString, + Computed: true, + }, + "parent_resource_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + } +} + +func expandResourceConfiguration(rConfigurations []interface{}) []ResourceConfigurationStruct { + configs := make([]ResourceConfigurationStruct, 0, len(rConfigurations)) + + for _, config := range rConfigurations { + configMap := config.(map[string]interface{}) + + rConfig := ResourceConfigurationStruct{ + ComponentName: configMap["component_name"].(string), + Configuration: configMap["configuration"].(map[string]interface{}), + Name: configMap["name"].(string), + Description: configMap["description"].(string), + DateCreated: configMap["date_created"].(string), + LastUpdated: configMap["last_updated"].(string), + ParentResourceID: configMap["parent_resource_id"].(string), + ResourceID: configMap["resource_id"].(string), + ResourceType: configMap["resource_type"].(string), + RequestID: configMap["request_id"].(string), + RequestState: configMap["request_state"].(string), + Owners: configMap["owners"].([]interface{}), + TenantID: configMap["tenant_id"].(string), + } + 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["component_name"] = componentName + helper["configuration"] = resourceDataMap + helper["name"] = config.Name + helper["date_created"] = config.DateCreated + helper["last_updated"] = config.LastUpdated + helper["owners"] = config.Owners + helper["resource_id"] = config.ResourceID + helper["request_id"] = config.RequestID + helper["parent_resource_id"] = config.ParentResourceID + helper["status"] = config.Status + helper["request_state"] = config.RequestState + helper["resource_type"] = config.ResourceType + helper["tenant_id"] = config.TenantID + helper["lease_start_date"] = config.Lease.Start + helper["lease_end_date"] = config.Lease.End + 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 19393b33..ab0ce855 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,10 +36,9 @@ type ProviderSchema struct { BusinessGroupName string WaitTimeout int RequestStatus string - FailedMessage string DeploymentConfiguration map[string]interface{} - ResourceConfiguration map[string]interface{} DeploymentDestroy bool + ResourceConfiguration []ResourceConfigurationStruct } func resourceVra7Deployment() *schema.Resource { @@ -49,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": { @@ -90,27 +91,16 @@ 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, - }, "deployment_destroy": { Type: schema.TypeBool, Optional: true, }, + "resource_configuration": resourceConfigurationSchema(), }, } } @@ -132,15 +122,21 @@ 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) - requestTemplate, validityErr := p.checkConfigValuesValidity(d) + validityErr := checkConfigValuesValidity(d) if validityErr != nil { return validityErr } - validityErr = p.checkResourceConfigValidity(requestTemplate) + + p, err := readProviderConfiguration(d) + if err != nil { + return err + } + + requestTemplate, validityErr := p.checkResourceConfigValidity() if validityErr != nil { return validityErr } @@ -159,31 +155,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.ComponentName == 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 } } } @@ -202,6 +187,7 @@ func resourceVra7DeploymentCreate(d *schema.ResourceData, meta interface{}) erro return err } d.SetId(catalogRequest.ID) + log.Info("Finished creating the resource vra7_deployment with request id %s", d.Id()) return resourceVra7DeploymentRead(d, meta) } @@ -217,19 +203,20 @@ 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() // Get client handle - p := readProviderConfiguration(d) - requestTemplate, validityErr := p.checkConfigValuesValidity(d) - if validityErr != nil { - return validityErr + p, err := readProviderConfiguration(d) + if err != nil { + return err } - validityErr = p.checkResourceConfigValidity(requestTemplate) + _, validityErr := p.checkResourceConfigValidity() if validityErr != nil { return validityErr } @@ -277,26 +264,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.ComponentName == 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 @@ -304,22 +285,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 } } @@ -328,6 +299,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) } @@ -335,7 +307,9 @@ 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. catalogItemRequestID := d.Id() @@ -350,55 +324,60 @@ 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{}) + var resourceConfigStruct ResourceConfigurationStruct + resourceConfigStruct.Configuration = rMap["data"].(map[string]interface{}) + resourceConfigStruct.Name = rMap["name"].(string) + resourceConfigStruct.DateCreated = rMap["dateCreated"].(string) + resourceConfigStruct.LastUpdated = rMap["lastUpdated"].(string) + resourceConfigStruct.ResourceID = rMap["resourceId"].(string) + resourceConfigStruct.ResourceType = rMap["resourceType"].(string) + resourceConfigStruct.RequestID = rMap["requestId"].(string) + resourceConfigStruct.TenantID = rMap["tenantId"].(string) + resourceConfigStruct.RequestState = rMap["requestState"].(string) + resourceConfigStruct.Owners = rMap["owners"].([]interface{}) + + if rMap["parentResourceId"] != nil { + resourceConfigStruct.ParentResourceID = rMap["parentResourceId"].(string) } - } - 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 rMap["description"] != nil { + resourceConfigStruct.Description = rMap["description"].(string) } + if rMap["status"] != nil { + resourceConfigStruct.Status = rMap["status"].(string) + } + + leaseMap := rMap["lease"].(map[string]interface{}) + resourceConfigStruct.Lease.Start = leaseMap["start"].(string) + resourceConfigStruct.Lease.End = leaseMap["end"].(string) + + resourceConfigList = append(resourceConfigList, resourceConfigStruct) } + if err := d.Set("resource_configuration", flattenResourceConfigurations(resourceConfigList)); err != nil { + return fmt.Errorf("error setting resource configuration - error: %v", err) + } + p, err := readProviderConfiguration(d) + if err != nil { + return err + } + d.Set("catalog_item_name", p.CatalogItemName) + d.Set("catalog_item_id", p.CatalogItemID) + d.Set("businessgroup_name", p.BusinessGroupName) + d.Set("businessgroup_id", p.BusinessGroupID) + + 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 client handle - p := readProviderConfiguration(d) + p, _ := readProviderConfiguration(d) //Get requester machine ID from schema.dataresource catalogItemRequestID := d.Id() // Throw an error if request ID has no value or empty value @@ -457,13 +436,21 @@ func resourceVra7DeploymentDelete(d *schema.ResourceData, meta interface{}) erro } } } + log.Info("Finished deleting the resource vra7_deployment....") return nil } // check if the resource configuration is valid in the terraform config file -func (p *ProviderSchema) checkResourceConfigValidity(requestTemplate *sdk.CatalogItemRequestTemplate) error { +func (p *ProviderSchema) checkResourceConfigValidity() (*sdk.CatalogItemRequestTemplate, error) { log.Info("Checking if the terraform config file is valid") + // Get request template for catalog item. + requestTemplate, err := vraClient.GetCatalogItemRequestTemplate(p.CatalogItemID) + if err != nil { + return nil, err + } + log.Info("The request template data corresponding to the catalog item %v is: \n %v\n", p.CatalogItemID, requestTemplate.Data) + // Get all component names in the blueprint corresponding to the catalog item. componentSet := make(map[string]bool) for field := range requestTemplate.Data { @@ -478,111 +465,110 @@ 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.ComponentName + 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 if len(invalidKeys) > 0 { log.Error("The resource_configuration in the config file has invalid component name(s): %v ", strings.Join(invalidKeys, ", ")) - return fmt.Errorf(ConfigInvalidError, strings.Join(invalidKeys, ", ")) + return nil, fmt.Errorf(ConfigInvalidError, strings.Join(invalidKeys, ", ")) } - return nil + + updateRequestTemplateFromDeploymentConfiguration(p.DeploymentConfiguration, requestTemplate) + requestTemplate.BusinessGroupID = p.BusinessGroupID + + return requestTemplate, nil } // check if the values provided in the config file are valid and set // them in the resource schema. Requires to call APIs -func (p *ProviderSchema) checkConfigValuesValidity(d *schema.ResourceData) (*sdk.CatalogItemRequestTemplate, error) { - // // If catalog_name and catalog_id both not provided then return an error - if len(p.CatalogItemName) <= 0 && len(p.CatalogItemID) <= 0 { - return nil, fmt.Errorf("Either catalog_name or catalog_id should be present in given configuration") +func checkConfigValuesValidity(d *schema.ResourceData) error { + + catalog_item_name := d.Get("catalog_item_name").(string) + catalog_item_id := d.Get("catalog_item_id").(string) + businessgroup_name := d.Get("businessgroup_name").(string) + businessgroup_id := d.Get("businessgroup_id").(string) + + // If catalog_item_name and catalog_item_id both not provided then return an error + if catalog_item_id == "" && catalog_item_name == "" { + return fmt.Errorf("Provide either a catalog_item_name or a catalog_item_id in the configuration") } - var catalogItemIDFromName string - var catalogItemNameFromID string - var err error - // if catalog item id is provided, fetch the catalog item name - if len(p.CatalogItemName) > 0 { - catalogItemIDFromName, err = vraClient.ReadCatalogItemByName(p.CatalogItemName) - if err != nil || catalogItemIDFromName == "" { - return nil, fmt.Errorf("Error in finding catalog item id corresponding to the catlog item name %v: \n %v", p.CatalogItemName, err) - } - log.Info("The catalog item id provided in the config is %v\n", catalogItemIDFromName) + // If both catalog_item_name and catalog_item_id return an error + if catalog_item_id != "" && catalog_item_name != "" { + return fmt.Errorf("Provide either a catalog_item_name or a catalog_item_id in the configuration") } - // if catalog item name is provided, fetch the catalog item id - if len(p.CatalogItemID) > 0 { // else if both are provided and matches or just id is provided, use id - catalogItemNameFromID, err = vraClient.ReadCatalogItemNameByID(p.CatalogItemID) - if err != nil || catalogItemNameFromID == "" { - return nil, fmt.Errorf("Error in finding catalog item name corresponding to the catlog item id %v: \n %v", p.CatalogItemID, err) - } - log.Info("The catalog item name corresponding to the catalog item id in the config is: %v\n", catalogItemNameFromID) + // If businessgroup_name and businessgroup_id both not provided then return an error + if businessgroup_id == "" && businessgroup_name == "" { + return fmt.Errorf("Provide either a businessgroup_id or a businessgroup_name in the configuration") } - // if both catalog item name and id are provided but does not belong to the same catalog item, throw an error - if len(p.CatalogItemName) > 0 && len(p.CatalogItemID) > 0 && (catalogItemIDFromName != p.CatalogItemID || catalogItemNameFromID != p.CatalogItemName) { - log.Error(CatalogItemIDNameNotMatchingErr, p.CatalogItemName, p.CatalogItemID) - return nil, fmt.Errorf(CatalogItemIDNameNotMatchingErr, p.CatalogItemName, p.CatalogItemID) - } else if len(p.CatalogItemID) > 0 { // else if both are provided and matches or just id is provided, use id - d.Set("catalog_item_id", p.CatalogItemID) - d.Set("catalog_item_name", catalogItemNameFromID) - } else if len(p.CatalogItemName) > 0 { // else if name is provided, use the id fetched from the name - d.Set("catalog_item_id", catalogItemIDFromName) - d.Set("catalog_item_name", p.CatalogItemName) + // If both businessgroup_name and businessgroup_id return an error + if businessgroup_id != "" && businessgroup_name != "" { + return fmt.Errorf("Provide either a businessgroup_id or a businessgroup_name in the configuration") } + return nil +} - // update the catalogItemID var with the updated id - p.CatalogItemID = d.Get("catalog_item_id").(string) +// read the config file +func readProviderConfiguration(d *schema.ResourceData) (*ProviderSchema, error) { - // Get request template for catalog item. - requestTemplate, err := vraClient.GetCatalogItemRequestTemplate(p.CatalogItemID) - if err != nil { - return nil, err + log.Info("Reading the provider configuration data.....") + providerSchema := ProviderSchema{ + CatalogItemName: strings.TrimSpace(d.Get("catalog_item_name").(string)), + CatalogItemID: strings.TrimSpace(d.Get("catalog_item_id").(string)), + Description: strings.TrimSpace(d.Get("description").(string)), + Reasons: strings.TrimSpace(d.Get("reasons").(string)), + BusinessGroupName: strings.TrimSpace(d.Get("businessgroup_name").(string)), + BusinessGroupID: strings.TrimSpace(d.Get("businessgroup_id").(string)), + WaitTimeout: d.Get("wait_timeout").(int) * 60, + ResourceConfiguration: expandResourceConfiguration(d.Get("resource_configuration").(*schema.Set).List()), + DeploymentDestroy: d.Get("deployment_destroy").(bool), + DeploymentConfiguration: d.Get("deployment_configuration").(map[string]interface{}), } - log.Info("The request template data corresponding to the catalog item %v is: \n %v\n", p.CatalogItemID, requestTemplate.Data) - updateRequestTemplateFromDeploymentConfiguration(p.DeploymentConfiguration, requestTemplate) + // if catalog item name is provided, fetch the catalog item id + if len(providerSchema.CatalogItemName) > 0 { + id, err := vraClient.ReadCatalogItemByName(providerSchema.CatalogItemName) + if err != nil { + return &providerSchema, err + } + providerSchema.CatalogItemID = id + } - // get the business group id from name - var businessGroupIDFromName string - if len(p.BusinessGroupName) > 0 { - businessGroupIDFromName, err = vraClient.GetBusinessGroupID(p.BusinessGroupName, vraClient.Tenant) - if err != nil || businessGroupIDFromName == "" { - return nil, err + // if catalog item id is provided, fetch the catalog item name + if len(providerSchema.CatalogItemID) > 0 { + name, _ := vraClient.ReadCatalogItemNameByID(providerSchema.CatalogItemID) + if name != "" { + providerSchema.CatalogItemName = name } } - //if both business group name and id are provided but does not belong to the same business group, throw an error - if len(p.BusinessGroupName) > 0 && len(p.BusinessGroupID) > 0 && businessGroupIDFromName != p.BusinessGroupID { - log.Error(BusinessGroupIDNameNotMatchingErr, p.BusinessGroupName, p.BusinessGroupID) - return nil, fmt.Errorf(BusinessGroupIDNameNotMatchingErr, p.BusinessGroupName, p.BusinessGroupID) - } else if len(p.BusinessGroupID) > 0 { // else if both are provided and matches or just id is provided, use id - log.Info("Setting business group id %s ", p.BusinessGroupID) - requestTemplate.BusinessGroupID = p.BusinessGroupID - } else if len(p.BusinessGroupName) > 0 { // else if name is provided, use the id fetched from the name - log.Info("Setting business group id %s for the group %s ", businessGroupIDFromName, p.BusinessGroupName) - requestTemplate.BusinessGroupID = businessGroupIDFromName + // get the business group id from name + if len(providerSchema.BusinessGroupName) > 0 { + id, err := vraClient.GetBusinessGroupID(providerSchema.BusinessGroupName, vraClient.Tenant) + if err != nil { + return &providerSchema, err + } + providerSchema.BusinessGroupID = id } - return requestTemplate, nil + + log.Info("The values provided in the TF config file is: \n %v ", providerSchema) + return &providerSchema, nil } func updateRequestTemplateFromDeploymentConfiguration(deploymentConfiguration map[string]interface{}, requestTemplate *sdk.CatalogItemRequestTemplate) { for field := range deploymentConfiguration { value := deploymentConfiguration[field] - requestTemplate.Data[field] = utils.UnmarshalJsonStringIfNecessary(field, value) + requestTemplate.Data[field] = utils.UnmarshalJSONStringIfNecessary(field, value) } } @@ -615,25 +601,3 @@ func waitForRequestCompletion(d *schema.ResourceData, meta interface{}, requestI // The user will need to use 'terraform refresh' at a later point to resolve this. return "", fmt.Errorf("Request has timed out with status %s. \nRun terraform refresh to get the latest state of your request", status) } - -// read the config file -func readProviderConfiguration(d *schema.ResourceData) *ProviderSchema { - - log.Info("Reading the provider configuration data.....") - providerSchema := ProviderSchema{ - CatalogItemName: strings.TrimSpace(d.Get("catalog_item_name").(string)), - CatalogItemID: strings.TrimSpace(d.Get("catalog_item_id").(string)), - Description: strings.TrimSpace(d.Get("description").(string)), - Reasons: strings.TrimSpace(d.Get("reasons").(string)), - 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{}), - DeploymentConfiguration: d.Get("deployment_configuration").(map[string]interface{}), - DeploymentDestroy: d.Get("deployment_destroy").(bool), - } - - log.Info("The values provided in the TF config file is: \n %v ", providerSchema) - return &providerSchema -} diff --git a/vra7/resource_vra7_deployment_test.go b/vra7/resource_vra7_deployment_test.go index e95bcd7e..d07480fc 100644 --- a/vra7/resource_vra7_deployment_test.go +++ b/vra7/resource_vra7_deployment_test.go @@ -41,7 +41,7 @@ func TestConfigValidityFunction(t *testing.T) { mockResourceData := schema.TestResourceDataRaw(t, resourceSchema, resourceDataMap) - p := readProviderConfiguration(mockResourceData) + p, _ := readProviderConfiguration(mockResourceData) readProviderConfiguration(mockResourceData) err = p.checkResourceConfigValidity(mockRequestTemplate) @@ -68,7 +68,7 @@ func TestConfigValidityFunction(t *testing.T) { } mockResourceData = schema.TestResourceDataRaw(t, resourceSchema, resourceDataMap) - p = readProviderConfiguration(mockResourceData) + p, _ = readProviderConfiguration(mockResourceData) var mockInvalidKeys []string mockInvalidKeys = append(mockInvalidKeys, "mock.machine3.vSphere.mock.cpu")