Preliminary fix for #7359. (#7369)

These changes contain a preliminary fix for #7359 in the Go SDK. The fix
handles input values that are nested one level deep within maps and
arrays, but does not handle other cases of nested input types.
This commit is contained in:
Pat Gavlin 2021-06-28 16:04:21 -07:00 committed by GitHub
parent e0902d2489
commit 398fb2f852
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 92 additions and 15 deletions

View file

@ -9,6 +9,9 @@
- [auto/nodejs] - Fail early when multiple versions of `@pulumi/pulumi` are detected in nodejs inline programs.'
[#7349](https://github.com/pulumi/pulumi/pull/7349)
- [sdk/go] - Add preliminary support for unmarshaling plain arrays and maps of output values.
[#7369](https://github.com/pulumi/pulumi/pull/7369)
### Bug Fixes
- [sdk/dotnet] - Fix swallowed nested exceptions with inline program, so they correctly bubble to the consumer.

View file

@ -218,29 +218,71 @@ func constructInputsCopyTo(ctx *Context, inputs map[string]interface{}, args int
continue
}
if field.Type.Implements(outputType) || field.Type.Implements(inputType) {
handleField := func(typ reflect.Type, value resource.PropertyValue, deps []Resource) (reflect.Value, error) {
resultType := anyOutputType
if field.Type.Implements(outputType) {
resultType = field.Type
} else if field.Type.Implements(inputType) {
toOutputMethodName := "To" + strings.TrimSuffix(field.Type.Name(), "Input") + "Output"
if toOutputMethod, found := field.Type.MethodByName(toOutputMethodName); found {
if typ.Implements(outputType) {
resultType = typ
} else if typ.Implements(inputType) {
toOutputMethodName := "To" + strings.TrimSuffix(typ.Name(), "Input") + "Output"
if toOutputMethod, found := typ.MethodByName(toOutputMethodName); found {
mt := toOutputMethod.Type
if mt.NumIn() == 0 && mt.NumOut() == 1 && mt.Out(0).Implements(outputType) {
resultType = mt.Out(0)
}
}
}
output := ctx.newOutput(resultType, ci.deps...)
output := ctx.newOutput(resultType, deps...)
dest := reflect.New(output.ElementType()).Elem()
known := !ci.value.ContainsUnknowns()
secret, err := unmarshalOutput(ctx, ci.value, dest)
secret, err := unmarshalOutput(ctx, value, dest)
if err != nil {
return reflect.Value{}, err
}
output.getState().resolve(dest.Interface(), known, secret, nil)
return reflect.ValueOf(output), nil
}
isInputType := func(typ reflect.Type) bool {
return typ.Implements(outputType) || typ.Implements(inputType)
}
if isInputType(field.Type) {
val, err := handleField(field.Type, ci.value, ci.deps)
if err != nil {
return err
}
fieldV.Set(val)
continue
}
output.getState().resolve(dest.Interface(), known, secret, nil)
fieldV.Set(reflect.ValueOf(output))
if field.Type.Kind() == reflect.Slice && isInputType(field.Type.Elem()) {
elemType := field.Type.Elem()
length := len(ci.value.ArrayValue())
dest := reflect.MakeSlice(field.Type, length, length)
for i := 0; i < length; i++ {
val, err := handleField(elemType, ci.value.ArrayValue()[i], ci.deps)
if err != nil {
return err
}
dest.Index(i).Set(val)
}
fieldV.Set(dest)
continue
}
if field.Type.Kind() == reflect.Map && isInputType(field.Type.Elem()) {
elemType := field.Type.Elem()
length := len(ci.value.ObjectValue())
dest := reflect.MakeMapWithSize(field.Type, length)
for k, v := range ci.value.ObjectValue() {
key := reflect.ValueOf(string(k))
val, err := handleField(elemType, v, ci.deps)
if err != nil {
return err
}
dest.SetMapIndex(key, val)
}
fieldV.Set(dest)
continue
}

View file

@ -21,6 +21,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)
@ -130,6 +131,14 @@ type NestedMapArgs struct {
Value map[string]Nested `pulumi:"value"`
}
type PlainArrayArgs struct {
Value []StringInput `pulumi:"value"`
}
type PlainMapArgs struct {
Value map[string]StringInput `pulumi:"value"`
}
func TestConstructInputsCopyTo(t *testing.T) {
var ctx Context
@ -143,6 +152,7 @@ func TestConstructInputsCopyTo(t *testing.T) {
expectedValue interface{}
expectedSecret bool
expectedDeps []Resource
typeOnly bool
}{
{
input: resource.NewStringProperty("foo"),
@ -339,23 +349,45 @@ func TestConstructInputsCopyTo(t *testing.T) {
"b": {Foo: "4", Bar: 4},
},
},
{
input: resource.NewArrayProperty([]resource.PropertyValue{
resource.NewStringProperty("foo"),
resource.NewStringProperty("bar"),
}),
args: &PlainArrayArgs{},
expectedType: []StringInput{},
typeOnly: true,
},
{
input: resource.NewObjectProperty(resource.NewPropertyMapFromMap(map[string]interface{}{
"foo": "bar",
"baz": "qux",
})),
args: &PlainMapArgs{},
expectedType: map[string]StringInput{},
typeOnly: true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%T-%v-%T", test.args, test.input, test.expectedType), func(t *testing.T) {
ctx, err := NewContext(context.Background(), RunInfo{})
assert.NoError(t, err)
require.NoError(t, err)
inputs := map[string]interface{}{
"value": &constructInput{value: test.input, deps: test.deps},
}
err = constructInputsCopyTo(ctx, inputs, test.args)
assert.NoError(t, err)
require.NoError(t, err)
result := reflect.ValueOf(test.args).Elem().FieldByName("Value").Interface()
assert.IsType(t, test.expectedType, result)
require.IsType(t, test.expectedType, result)
if _, ok := test.expectedType.(Output); ok {
value, known, secret, deps, err := await(result.(Output))
if test.typeOnly {
return
}
if out, ok := result.(Output); ok {
value, known, secret, deps, err := await(out)
assert.NoError(t, err)
assert.Equal(t, test.expectedValue, value)
assert.True(t, known)