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:
parent
653dcf8f1f
commit
edf8bf5c30
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue