Support map-typed inputs in Go SDK RegisterResource (#4521)

Adds support for RegisterResource to accept map-typed implementations if Input as well as the existing struct-typed implementations. Currently these must be fully untyped - but both map[string]pulumi.Input and map[string]interface{} are allowed. In the future, it's plausible that a mode where the data itself is a map, but the ElementType implementation returns a struct could be supported, with the struct used to provide type information over the untyped map.
This commit is contained in:
Luke Hoban 2020-04-30 11:56:47 -07:00 committed by GitHub
parent 653dcf8f1f
commit edf8bf5c30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 115 additions and 35 deletions

View file

@ -6,6 +6,9 @@ CHANGELOG
- Add support for generating Fish completions
[#4401](https://github.com/pulumi/pulumi/pull/4401)
- Support map-typed inputs in RegisterResource for Go SDK
[#4522](https://github.com/pulumi/pulumi/pull/4522)
- Don't call IMocks.NewResourceAsync for the root stack resource
[#4527](https://github.com/pulumi/pulumi/pull/4527)

View file

@ -411,8 +411,9 @@ func (ctx *Context) RegisterResource(
if propsType.Kind() == reflect.Ptr {
propsType = propsType.Elem()
}
if propsType.Kind() != reflect.Struct {
return errors.New("props must be a struct or a pointer to a struct")
if !(propsType.Kind() == reflect.Struct ||
(propsType.Kind() == reflect.Map && propsType.Key().Kind() == reflect.String)) {
return errors.New("props must be a struct or map or a pointer to a struct or map")
}
}

View file

@ -70,38 +70,11 @@ func marshalInputs(props Input) (resource.PropertyMap, map[string][]URN, []URN,
return pmap, pdeps, depURNs, nil
}
pv := reflect.ValueOf(props)
if pv.Kind() == reflect.Ptr {
if pv.IsNil() {
return pmap, pdeps, depURNs, nil
}
pv = pv.Elem()
}
pt := pv.Type()
contract.Assert(pt.Kind() == reflect.Struct)
// We use the resolved type to decide how to convert inputs to outputs.
rt := props.ElementType()
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
contract.Assert(rt.Kind() == reflect.Struct)
getMappedField := mapStructTypes(pt, rt)
// Now, marshal each field in the input.
numFields := pt.NumField()
for i := 0; i < numFields; i++ {
destField, _ := getMappedField(reflect.Value{}, i)
tag := destField.Tag.Get("pulumi")
if tag == "" {
continue
}
marshalProperty := func(pname string, pv interface{}, pt reflect.Type) error {
// Get the underlying value, possibly waiting for an output to arrive.
v, resourceDeps, err := marshalInput(pv.Field(i).Interface(), destField.Type, true)
v, resourceDeps, err := marshalInput(pv, pt, true)
if err != nil {
return nil, nil, nil, fmt.Errorf("awaiting input property %s: %w", tag, err)
return fmt.Errorf("awaiting input property %s: %w", pname, err)
}
// Record all dependencies accumulated from reading this property.
@ -110,7 +83,7 @@ func marshalInputs(props Input) (resource.PropertyMap, map[string][]URN, []URN,
for _, dep := range resourceDeps {
depURN, _, _, err := dep.URN().awaitURN(context.TODO())
if err != nil {
return nil, nil, nil, err
return err
}
if !pdepset[depURN] {
deps = append(deps, depURN)
@ -122,12 +95,63 @@ func marshalInputs(props Input) (resource.PropertyMap, map[string][]URN, []URN,
}
}
if len(deps) > 0 {
pdeps[tag] = deps
pdeps[pname] = deps
}
if !v.IsNull() || len(deps) > 0 {
pmap[resource.PropertyKey(tag)] = v
pmap[resource.PropertyKey(pname)] = v
}
return nil
}
pv := reflect.ValueOf(props)
if pv.Kind() == reflect.Ptr {
if pv.IsNil() {
return pmap, pdeps, depURNs, nil
}
pv = pv.Elem()
}
pt := pv.Type()
rt := props.ElementType()
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
switch pt.Kind() {
case reflect.Struct:
contract.Assert(rt.Kind() == reflect.Struct)
// We use the resolved type to decide how to convert inputs to outputs.
rt := props.ElementType()
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
getMappedField := mapStructTypes(pt, rt)
// Now, marshal each field in the input.
numFields := pt.NumField()
for i := 0; i < numFields; i++ {
destField, _ := getMappedField(reflect.Value{}, i)
tag := destField.Tag.Get("pulumi")
if tag == "" {
continue
}
err := marshalProperty(tag, pv.Field(i).Interface(), destField.Type)
if err != nil {
return nil, nil, nil, err
}
}
case reflect.Map:
contract.Assert(rt.Key().Kind() == reflect.String)
for _, key := range pv.MapKeys() {
keyname := key.Interface().(string)
val := pv.MapIndex(key).Interface()
err := marshalProperty(keyname, val, rt.Elem())
if err != nil {
return nil, nil, nil, err
}
}
default:
return nil, nil, nil, fmt.Errorf("cannot marshal Input that is not a struct or map, saw type %s", pt.String())
}
return pmap, pdeps, depURNs, nil

View file

@ -492,3 +492,55 @@ func TestMarshalRoundtripNestedSecret(t *testing.T) {
}
}
}
type simpleResource struct {
CustomResourceState
}
type UntypedArgs map[string]interface{}
func (UntypedArgs) ElementType() reflect.Type {
return reflect.TypeOf((*map[string]interface{})(nil)).Elem()
}
func TestMapInputMarhsalling(t *testing.T) {
var theResource simpleResource
out := newOutput(reflect.TypeOf((*StringOutput)(nil)).Elem(), &theResource)
out.resolve("outputty", true, false)
inputs1 := Map(map[string]Input{
"prop": out,
"nested": Map(map[string]Input{
"foo": String("foo"),
"bar": Int(42),
}),
})
inputs2 := UntypedArgs(map[string]interface{}{
"prop": "outputty",
"nested": map[string]interface{}{
"foo": "foo",
"bar": 42,
},
})
cases := []struct {
inputs Input
depUrns []string
}{
{inputs: inputs1, depUrns: []string{""}},
{inputs: inputs2, depUrns: nil},
}
for _, c := range cases {
resolved, _, depUrns, err := marshalInputs(c.inputs)
assert.NoError(t, err)
assert.Equal(t, "outputty", resolved["prop"].StringValue())
assert.Equal(t, "foo", resolved["nested"].ObjectValue()["foo"].StringValue())
assert.Equal(t, 42.0, resolved["nested"].ObjectValue()["bar"].NumberValue())
assert.Equal(t, len(c.depUrns), len(depUrns))
for i := range c.depUrns {
assert.Equal(t, URN(c.depUrns[i]), depUrns[i])
}
}
}