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

Fix set marshaling. #104

Merged
merged 2 commits into from
Jan 24, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 109 additions & 17 deletions pkg/tfbridge/schema.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ package tfbridge

import (
"fmt"
"strconv"

"github.com/golang/glog"

@@ -145,22 +146,23 @@ func MakeTerraformInput(res *PulumiResource, name string,
oldArr = old.ArrayValue()
}

var etfs *schema.Schema
if tfs != nil {
if sch, issch := tfs.Elem.(*schema.Schema); issch {
etfs = sch
} else if _, isres := tfs.Elem.(*schema.Resource); isres {
// The IsObject case below expects a schema whose `Elem` is
// a Resource, so just pass the full List schema
etfs = tfs
}
}
var eps *SchemaInfo
if ps != nil {
eps = ps.Elem
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We hoisted this because it's invariant, right? I.e. this wasn't a change in behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. I can undo this if you'd like; it isn't necessary for this fix. It is an artifact of an earlier attempt.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's take it with this PR but split it into a different commit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

var arr []interface{}
for i, elem := range v.ArrayValue() {
var etfs *schema.Schema
if tfs != nil {
if sch, issch := tfs.Elem.(*schema.Schema); issch {
etfs = sch
} else if _, isres := tfs.Elem.(*schema.Resource); isres {
// The IsObject case below expects a schema whose `Elem` is
// a Resource, so just pass the full List schema
etfs = tfs
}
}
var eps *SchemaInfo
if ps != nil {
eps = ps.Elem
}
var oldElem resource.PropertyValue
if i < len(oldArr) {
oldElem = oldArr[i]
@@ -376,7 +378,7 @@ func MakeTerraformAttributes(res *PulumiResource, m resource.PropertyMap,
if err != nil {
return nil, err
}
return MakeTerraformAttributesFromInputs(inputs), nil
return MakeTerraformAttributesFromInputs(inputs, tfs)
}

// MakeTerraformAttributesFromRPC unmarshals an RPC property map and calls through to MakeTerraformAttributes.
@@ -391,9 +393,99 @@ func MakeTerraformAttributesFromRPC(res *PulumiResource, m *pbstruct.Struct,
return MakeTerraformAttributes(res, props, tfs, ps, defaults)
}

// flattenValue takes a single value and recursively flattens its properties into the given string -> string map under
// the provided prefix. It expects that the value has been "schema-fied" by being read out of a schema.FieldReader (in
// particular, all sets *must* be represented as schema.Set values). The flattened value may then be used as the value
// of a terraform.InstanceState.Attributes field.
//
// Note that this duplicates much of the logic in TF's schema.MapFieldWriter. Ideally, we would just use that type,
// but there are various API/implementation challenges that preclude that option. The most worrying (and potentially
// fragile) piece of duplication is the code that calculates a set member's hash code; see the code under
// `case *schema.Set`.
func flattenValue(result map[string]string, prefix string, value interface{}) {
if value == nil {
return
}

switch t := value.(type) {
case bool:
if t {
result[prefix] = "true"
} else {
result[prefix] = "false"
}
case int:
result[prefix] = strconv.FormatInt(int64(t), 10)
case float64:
result[prefix] = strconv.FormatFloat(t, 'G', -1, 64)
case string:
result[prefix] = t
case []interface{}:
// Flatten each element.
for i, elem := range t {
flattenValue(result, prefix+"."+strconv.FormatInt(int64(i), 10), elem)
}

// Set the count.
result[prefix+".#"] = strconv.FormatInt(int64(len(t)), 10)
case *schema.Set:
// Flatten each element.
setList := t.List()
for _, elem := range setList {
// Note that the logic below is duplicated from `scheme.Set.hash`. If that logic ever changes, this will
// need to change in kind.
code := t.F(elem)
if code < 0 {
code = -code
}

flattenValue(result, prefix+"."+strconv.Itoa(code), elem)
}

// Set the count.
result[prefix+".#"] = strconv.FormatInt(int64(len(setList)), 10)
case map[string]interface{}:
for k, v := range t {
flattenValue(result, prefix+"."+k, v)
}

// Set the count.
result[prefix+".%"] = strconv.Itoa(len(t))
default:
contract.Failf("Unexpected TF input value: %v", t)
}
}

// MakeTerraformAttributesFromInputs creates a flat Terraform map from a structured set of Terraform inputs.
func MakeTerraformAttributesFromInputs(inputs map[string]interface{}) map[string]string {
return flatmap.Flatten(inputs)
func MakeTerraformAttributesFromInputs(inputs map[string]interface{},
tfs map[string]*schema.Schema) (map[string]string, error) {

// In order to flatten the TF inputs into a TF attribute map, we must first schema-ify them by reading them out of
// a FieldReader. The most straightforward way to do this is to turn the inputs into a TF config.Config value and
// use the same to create a schema.ConfigFieldReader.
cfg, err := MakeTerraformConfigFromInputs(inputs)
if err != nil {
return nil, err
}

// Read each top-level value out of the config we created above using a ConfigFieldReader and recursively flatten
// them into their TF attribute form. The result is our set of TF attributes.
result := make(map[string]string)
reader := &schema.ConfigFieldReader{Config: cfg, Schema: tfs}
for k, v := range inputs {
// Elide nil values.
if v == nil {
continue
}

f, err := reader.ReadField([]string{k})
if err != nil {
return nil, errors.Wrapf(err, "could not read field %v", k)
}

flattenValue(result, k, f.Value)
}
return result, nil
}

// MakeTerraformDiff takes a bag of old and new properties, and returns two things: the existing resource's state as
146 changes: 146 additions & 0 deletions pkg/tfbridge/schema_test.go
Original file line number Diff line number Diff line change
@@ -176,6 +176,152 @@ func TestTerraformOutputs(t *testing.T) {
}), result)
}

func TestTerraformAttributes(t *testing.T) {
result, err := MakeTerraformAttributesFromInputs(
map[string]interface{}{
"nil_property_value": nil,
"bool_property_value": false,
"number_property_value": 42,
"float_property_value": 99.6767932,
"string_property_value": "ognirts",
"array_property_value": []interface{}{"an array"},
"object_property_value": map[string]interface{}{
"property_a": "a",
"property_b": true,
},
"map_property_value": map[string]interface{}{
"propertyA": "a",
"propertyB": true,
"propertyC": map[string]interface{}{
"nestedPropertyA": true,
},
},
"nested_resources": []interface{}{
map[string]interface{}{
"configuration": map[string]interface{}{
"configurationValue": true,
},
},
},
"set_property_value": []interface{}{"set member 1", "set member 2"},
},
map[string]*schema.Schema{
"nil_property_value": {Type: schema.TypeMap},
"bool_property_value": {Type: schema.TypeBool},
"number_property_value": {Type: schema.TypeInt},
"float_property_value": {Type: schema.TypeFloat},
"string_property_value": {Type: schema.TypeString},
"array_property_value": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
},
"object_property_value": {Type: schema.TypeMap},
"map_property_value": {Type: schema.TypeMap},
"nested_resources": {
Type: schema.TypeList,
MaxItems: 1,
// Embed a `*schema.Resource` to validate that type directed
// walk of the schema successfully walks inside Resources as well
// as Schemas.
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"configuration": {Type: schema.TypeMap},
},
},
},
"set_property_value": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
},
})

assert.NoError(t, err)
assert.Equal(t, result, map[string]string{
"array_property_value.#": "1",
"array_property_value.0": "an array",
"bool_property_value": "false",
"float_property_value": "99.6767932",
"map_property_value.%": "3",
"map_property_value.propertyA": "a",
"map_property_value.propertyB": "true",
"map_property_value.propertyC.%": "1",
"map_property_value.propertyC.nestedPropertyA": "true",
"nested_resources.#": "1",
"nested_resources.0.%": "1",
"nested_resources.0.configuration.%": "1",
"nested_resources.0.configuration.configurationValue": "true",
"number_property_value": "42",
"object_property_value.%": "2",
"object_property_value.property_a": "a",
"object_property_value.property_b": "true",
"set_property_value.#": "2",
"set_property_value.3618983862": "set member 2",
"set_property_value.4237827189": "set member 1",
"string_property_value": "ognirts",
})

// MapFieldWriter has issues with values of TypeMap. Build a schema without such values s.t. we can test
// MakeTerraformAttributes against the output of MapFieldWriter.
sharedSchema := map[string]*schema.Schema{
"bool_property_value": {Type: schema.TypeBool},
"number_property_value": {Type: schema.TypeInt},
"float_property_value": {Type: schema.TypeFloat},
"string_property_value": {Type: schema.TypeString},
"array_property_value": {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
},
"nested_resource_value": {
Type: schema.TypeList,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"nested_set_property": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
},
"nested_string_property": {Type: schema.TypeString},
},
},
},
"set_property_value": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
},
}
sharedInputs := map[string]interface{}{
"bool_property_value": false,
"number_property_value": 42,
"float_property_value": 99.6767932,
"string_property_value": "ognirts",
"array_property_value": []interface{}{"an array"},
"nested_resource_value": map[string]interface{}{
"nested_set_property": []interface{}{"nested set member"},
"nested_string_property": "value",
},
"set_property_value": []interface{}{"set member 1", "set member 2"},
}

// Build a TF attribute map using schema.MapFieldWriter.
cfg, err := MakeTerraformConfigFromInputs(sharedInputs)
assert.NoError(t, err)
reader := &schema.ConfigFieldReader{Config: cfg, Schema: sharedSchema}
writer := &schema.MapFieldWriter{Schema: sharedSchema}
for k := range sharedInputs {
f, ferr := reader.ReadField([]string{k})
assert.NoError(t, ferr)

err = writer.WriteField([]string{k}, f.Value)
assert.NoError(t, err)
}
expected := writer.Map()

// Build the same using MakeTerraformAttributesFromInputs.
result, err = MakeTerraformAttributesFromInputs(sharedInputs, sharedSchema)
assert.NoError(t, err)
assert.Equal(t, expected, result)
}

func TestDefaults(t *testing.T) {
// Produce maps with the following properties, and then validate them:
// - aaa string; no defaults, no inputs => empty