Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FeatureRequest: Issue #352] Support for write-only properties #353

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions docs/how_to.md
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@ x-terraform-id | boolean | If this meta attribute is present in an object defini
x-terraform-field-name | string | This enables service providers to override the schema definition property name with a different one which will be the property name used in the terraform configuration file. This is mostly used to expose the internal property to a more user friendly name. If the extension is not present and the property name is not terraform compliant (following snake_case), an automatic conversion will be performed by the OpenAPI Terraform provider to make the name compliant (following Terraform's field name convention to be snake_case)
x-terraform-field-status | boolean | If this meta attribute is present in a definition property, the value will be used as the status identifier when executing the polling mechanism on eligible async operations such as POST/PUT/DELETE.
[x-terraform-ignore-order](#xTerraformIgnoreOrder) | boolean | If this meta attribute is present in a definition property of type list, when the plugin is updating the state for the property it will inspect the items of the list received from remote and compare with the local values and if the lists are the same but unordered the state will keep the users input. Please go to the `x-terraform-ignore-order` section to learn more about the different behaviours supported.
x-terraform-write-only | boolean | If this meta attribute is present in a definition property, when the plugin is reading or updating the state for the property it will always take the local state's value as the the value of the property. Any changes in the remote state for such properties will be ignored.

###### <a name="xTerraformIgnoreOrder">x-terraform-ignore-order</a>

Expand Down
151 changes: 102 additions & 49 deletions openapi/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ func updateStateWithPayloadDataAndOptions(openAPIResource SpecResource, remoteDa
}

propValue := propertyRemoteValue
propertyLocalStateValue := resourceLocalData.Get(property.GetTerraformCompliantPropertyName())
if ignoreListOrderEnabled && property.shouldIgnoreOrder() {
desiredValue := resourceLocalData.Get(property.GetTerraformCompliantPropertyName())
propValue = processIgnoreOrderIfEnabled(*property, desiredValue, propertyRemoteValue)
propValue = processIgnoreOrderIfEnabled(*property, propertyLocalStateValue, propertyRemoteValue)
}

value, err := convertPayloadToLocalStateDataValue(property, propValue)
value, err := convertPayloadToLocalStateDataValue(property, propValue, propertyLocalStateValue)
if err != nil {
return err
}
Expand Down Expand Up @@ -194,72 +194,125 @@ func processIgnoreOrderIfEnabled(property SpecSchemaDefinitionProperty, inputPro
return remoteValue
}

func convertPayloadToLocalStateDataValue(property *SpecSchemaDefinitionProperty, propertyValue interface{}) (interface{}, error) {
if propertyValue == nil {
return nil, nil
func convertPayloadToLocalStateDataValue(property *SpecSchemaDefinitionProperty, propertyValue interface{}, propertyLocalStateValue interface{}) (interface{}, error) {
if property.WriteOnly {
return propertyLocalStateValue, nil
}
dataValueKind := reflect.TypeOf(propertyValue).Kind()
switch dataValueKind {
case reflect.Map:
objectInput := map[string]interface{}{}
mapValue := propertyValue.(map[string]interface{})
for propertyName, propertyValue := range mapValue {
schemaDefinitionProperty, err := property.SpecSchemaDefinition.getProperty(propertyName)
if err != nil {
return nil, err
}
var propValue interface{}
// Here we are processing the items of the list which are objects. In this case we need to keep the original
// types as Terraform honors property types for resource schemas attached to TypeList properties
propValue, err = convertPayloadToLocalStateDataValue(schemaDefinitionProperty, propertyValue)
if err != nil {
return nil, err
}
objectInput[schemaDefinitionProperty.GetTerraformCompliantPropertyName()] = propValue
}

// This is the work around put in place to have support for complex objects considering terraform sdk limitation to use
// blocks only for TypeList and TypeSet . In this case, we need to make sure that the json (which reflects to a map)
// gets translated to the expected array of one item that terraform expects.
if property.shouldUseLegacyTerraformSDKBlockApproachForComplexObjects() {
arrayInput := []interface{}{}
arrayInput = append(arrayInput, objectInput)
return arrayInput, nil
}
return objectInput, nil
case reflect.Slice, reflect.Array:
switch property.Type {
case TypeObject:
return convertObjectToLocalStateData(property, propertyValue, propertyLocalStateValue)
case TypeList:
if isListOfPrimitives, _ := property.isTerraformListOfSimpleValues(); isListOfPrimitives {
return propertyValue, nil
}
if property.isArrayOfObjectsProperty() {
arrayInput := []interface{}{}
arrayValue := propertyValue.([]interface{})
for _, arrayItem := range arrayValue {
objectValue, err := convertPayloadToLocalStateDataValue(property, arrayItem)

arrayValue := make([]interface{}, 0)
if propertyValue != nil {
arrayValue = propertyValue.([]interface{})
}

localStateArrayValue := make([]interface{}, 0)
if propertyLocalStateValue != nil {
localStateArrayValue = propertyLocalStateValue.([]interface{})
}

for arrayIdx := 0; arrayIdx < intMax(len(arrayValue), len(localStateArrayValue)); arrayIdx++ {
var arrayItem interface{} = nil
if arrayIdx < len(arrayValue) {
arrayItem = arrayValue[arrayIdx]
}
var localStateArrayItem interface{} = nil
if arrayIdx < len(localStateArrayValue) {
localStateArrayItem = localStateArrayValue[arrayIdx]
}
objectValue, err := convertObjectToLocalStateData(property, arrayItem, localStateArrayItem)
if err != nil {
return err, nil
}
arrayInput = append(arrayInput, objectValue)
if objectValue != nil {
arrayInput = append(arrayInput, objectValue)
}
}
return arrayInput, nil
}
return nil, fmt.Errorf("property '%s' is supposed to be an array objects", property.Name)
case reflect.String:
case TypeString:
if propertyValue == nil {
return nil, nil
}
return propertyValue.(string), nil
case reflect.Int:
return propertyValue.(int), nil
case reflect.Float64:
// In golang, a number in JSON message is always parsed into float64. Hence, checking here if the property value is
// an actual int or if not then casting to float64
if property.Type == TypeInt {
return int(propertyValue.(float64)), nil
case TypeInt:
if propertyValue == nil {
return nil, nil
}
// In golang, a number in JSON message is always parsed into float64, however testing/internal use can define the property value as a proper int.
if reflect.TypeOf(propertyValue).Kind() == reflect.Int {
return propertyValue.(int), nil
}
return int(propertyValue.(float64)), nil
case TypeFloat:
if propertyValue == nil {
return nil, nil
}
return propertyValue.(float64), nil
case reflect.Bool:
case TypeBool:
if propertyValue == nil {
return nil, nil
}
return propertyValue.(bool), nil
default:
return nil, fmt.Errorf("'%s' type not supported", dataValueKind)
return nil, fmt.Errorf("'%s' type not supported", property.Type)
}
}

func convertObjectToLocalStateData(property *SpecSchemaDefinitionProperty, propertyValue interface{}, propertyLocalStateValue interface{}) (interface{}, error) {
objectInput := map[string]interface{}{}

mapValue := make(map[string]interface{})
if propertyValue != nil {
mapValue = propertyValue.(map[string]interface{})
}

localStateMapValue := make(map[string]interface{})
if propertyLocalStateValue != nil {
if reflect.TypeOf(propertyLocalStateValue).Kind() == reflect.Map {
localStateMapValue = propertyLocalStateValue.(map[string]interface{})
} else if reflect.TypeOf(propertyLocalStateValue).Kind() == reflect.Slice && len(propertyLocalStateValue.([]interface{})) == 1 {
localStateMapValue = propertyLocalStateValue.([]interface{})[0].(map[string]interface{}) // local state can store nested objects as a single item array
}
}

for _, schemaDefinitionProperty := range property.SpecSchemaDefinition.Properties {
propertyName := schemaDefinitionProperty.Name
propertyValue := mapValue[propertyName]

// Here we are processing the items of the list which are objects. In this case we need to keep the original
// types as Terraform honors property types for resource schemas attached to TypeList properties
propValue, err := convertPayloadToLocalStateDataValue(schemaDefinitionProperty, propertyValue, localStateMapValue[schemaDefinitionProperty.GetTerraformCompliantPropertyName()])
if err != nil {
return nil, err
}
if propValue != nil {
objectInput[schemaDefinitionProperty.GetTerraformCompliantPropertyName()] = propValue
}
}

if len(objectInput) == 0 {
return nil, nil
}

// This is the work around put in place to have support for complex objects considering terraform sdk limitation to use
// blocks only for TypeList and TypeSet . In this case, we need to make sure that the json (which reflects to a map)
// gets translated to the expected array of one item that terraform expects.
if property.shouldUseLegacyTerraformSDKBlockApproachForComplexObjects() {
arrayInput := []interface{}{}
arrayInput = append(arrayInput, objectInput)
return arrayInput, nil
}
return objectInput, nil
}

// setResourceDataProperty sets the expectedValue for the given schemaDefinitionPropertyName using the terraform compliant property name
Expand Down
Loading