[sdk/go] Fix regression marshaling assets/archives (#8290)

This change fixes a regression marshaling assets/archives that was introduced after adding support for marshaling output values.

For example, when setting an `Asset` on a field typed as `AssetOrAchiveInput`, in `marshalInput`, the input was being converted into an `AssetOrArchiveOutput` via the `ToAssetOrArchiveOutputWithContext`. Awaiting the output returns an `*asset` struct, which is itself an `AssetInput`, which causes infinite recursion when passed recursively to `marshalInput`.

The fix is to skip the `Input` checks in recursive calls, which is equivalent to the previous behavior before the regression was introduced.

Another issue was that when the input is converted to an output, this would result in `marshalInput` always returning an output value, even if the original passed-in value was not an output. To address this, if the output's value is known, not a secret, and has no dependencies, we can return the value itself rather than wrapping it as an output.
This commit is contained in:
Justin Van Patten 2021-10-27 08:44:42 -07:00 committed by GitHub
parent 530641576d
commit fbf62399ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 219 additions and 30 deletions

View file

@ -18,3 +18,6 @@
- [sdk/dotnet] - Fix a race condition when detecting exceptions in stack creation
[#8294](https://github.com/pulumi/pulumi/pull/8294)
- [sdk/go] - Fix regression marshaling assets/archives.
[#8290](https://github.com/pulumi/pulumi/pull/8290)

View file

@ -1504,13 +1504,7 @@ func TestConstructResult(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, resource.PropertyMap{
"foo": resource.NewOutputProperty(resource.Output{
Element: resource.NewStringProperty("hi"),
Known: true,
}),
"someValue": resource.NewOutputProperty(resource.Output{
Element: resource.NewStringProperty("something"),
Known: true,
}),
"foo": resource.NewStringProperty("hi"),
"someValue": resource.NewStringProperty("something"),
}, resolvedProps)
}

View file

@ -214,12 +214,20 @@ const cannotAwaitFmt = "cannot marshal Output value of type %T; please use Apply
// marshalInput marshals an input value, returning its raw serializable value along with any dependencies.
func marshalInput(v interface{}, destType reflect.Type, await bool) (resource.PropertyValue, []Resource, error) {
return marshalInputImpl(v, destType, await, false /*skipInputCheck*/)
}
// marshalInputImpl marshals an input value, returning its raw serializable value along with any dependencies.
func marshalInputImpl(v interface{},
destType reflect.Type,
await,
skipInputCheck bool) (resource.PropertyValue, []Resource, error) {
var deps []Resource
for {
valueType := reflect.TypeOf(v)
// If this is an Input, make sure it is of the proper type and await it if it is an output/
if input, ok := v.(Input); ok {
if input, ok := v.(Input); !skipInputCheck && ok {
if inputType := reflect.ValueOf(input); inputType.Kind() == reflect.Ptr && inputType.IsNil() {
// input type is a ptr type with a nil backing value
return resource.PropertyValue{}, nil, nil
@ -259,34 +267,45 @@ func marshalInput(v interface{}, destType reflect.Type, await bool) (resource.Pr
return resource.PropertyValue{}, nil, err
}
// Get the underlying value, if known.
var element resource.PropertyValue
if known {
element, _, err = marshalInputImpl(ov, destType, await, true /*skipInputCheck*/)
if err != nil {
return resource.PropertyValue{}, nil, err
}
// If it's known, not a secret, and has no deps, return the value itself.
if !secret && len(outputDeps) == 0 {
return element, nil, nil
}
}
// Expand dependencies.
urnSet, err := expandDependencies(context.TODO(), outputDeps)
if err != nil {
return resource.PropertyValue{}, nil, err
}
urns := urnSet.sortedValues()
var dependencies []resource.URN
if len(urns) > 0 {
dependencies = make([]resource.URN, len(urns))
for i, urn := range urns {
if len(urnSet) > 0 {
dependencies = make([]resource.URN, len(urnSet))
for i, urn := range urnSet.sortedValues() {
dependencies[i] = resource.URN(urn)
}
}
out := resource.Output{
return resource.NewOutputProperty(resource.Output{
Element: element,
Known: known,
Secret: secret,
Dependencies: dependencies,
}
if known {
out.Element, _, err = marshalInput(ov, destType, await)
if err != nil {
return resource.PropertyValue{}, nil, err
}
}
return resource.NewOutputProperty(out), outputDeps, nil
}), outputDeps, nil
}
}
// Set skipInputCheck to false, so that if we loop around we don't skip the input check.
skipInputCheck = false
// If v is nil, just return that.
if v == nil {
return resource.PropertyValue{}, nil, nil

View file

@ -915,20 +915,29 @@ func TestOutputValueMarshalling(t *testing.T) {
out := ctx.newOutput(anyOutputType, resources...)
out.getState().resolve(value.value, known, secret, nil)
inputs := Map{"value": out}
expected := resource.Output{
Known: known,
Secret: secret,
Dependencies: deps,
expectedValue := value.expected
if !known || secret || len(deps) > 0 {
v := resource.Output{
Known: known,
Secret: secret,
Dependencies: deps,
}
if known {
v.Element = value.expected
}
expectedValue = resource.NewOutputProperty(v)
}
if known {
expected.Element = value.expected
expected := resource.PropertyMap{"value": expectedValue}
if value.value == nil && known && !secret && len(deps) == 0 {
// marshalInputs excludes plain nil values.
expected = resource.PropertyMap{}
}
name := fmt.Sprintf("value=%v, known=%v, secret=%v, deps=%v", value, known, secret, deps)
t.Run(name, func(t *testing.T) {
inputs := Map{"value": out}
expected := resource.PropertyMap{"value": resource.NewOutputProperty(expected)}
actual, _, _, err := marshalInputs(inputs)
assert.NoError(t, err)
assert.Equal(t, expected, actual)
@ -1129,6 +1138,42 @@ func (o TemplateTagSpecificationArrayOutput) ToTemplateTagSpecificationArrayOutp
return o
}
type bucketObjectArgs struct {
Source AssetOrArchive `pulumi:"source"`
}
type BucketObjectArgs struct {
Source AssetOrArchiveInput
}
func (BucketObjectArgs) ElementType() reflect.Type {
return reflect.TypeOf((*bucketObjectArgs)(nil)).Elem()
}
type myResourceArgs struct {
Res Resource `pulumi:"res"`
}
type MyResourceArgs struct {
Res ResourceInput
}
func (MyResourceArgs) ElementType() reflect.Type {
return reflect.TypeOf((*myResourceArgs)(nil)).Elem()
}
type myNestedOutputArgs struct {
Nested interface{} `pulumi:"nested"`
}
type MyNestedOutputArgs struct {
Nested Input
}
func (MyNestedOutputArgs) ElementType() reflect.Type {
return reflect.TypeOf((*myNestedOutputArgs)(nil)).Elem()
}
func TestOutputValueMarshallingNested(t *testing.T) {
ctx, err := NewContext(context.Background(), RunInfo{})
assert.Nil(t, err)
@ -1149,6 +1194,26 @@ func TestOutputValueMarshallingNested(t *testing.T) {
unknownStringOutput := ctx.newOutput(stringOutputType).(StringOutput)
unknownStringOutput.getState().resolve("", false /*known*/, false /*secret*/, nil)
assetOutputType := reflect.TypeOf((*AssetOutput)(nil)).Elem()
fileAssetOutput := ctx.newOutput(assetOutputType).(AssetOutput)
fileAssetOutput.getState().resolve(&asset{path: "foo.txt"}, true /*known*/, false /*secret*/, nil)
fileAssetSecretOutput := ctx.newOutput(assetOutputType).(AssetOutput)
fileAssetSecretOutput.getState().resolve(&asset{path: "foo.txt"}, true /*known*/, true /*secret*/, nil)
fileAssetOutputDeps := ctx.newOutput(assetOutputType).(AssetOutput)
fileAssetOutputDeps.getState().resolve(&asset{path: "foo.txt"}, true /*known*/, false, /*secret*/
[]Resource{newSimpleCustomResource(ctx, "fakeURN", "fakeID")})
anyOutputType := reflect.TypeOf((*AnyOutput)(nil)).Elem()
nestedOutput := ctx.newOutput(anyOutputType).(AnyOutput)
nestedOutput.getState().resolve(fileAssetOutput, true /*known*/, false /*secret*/, nil)
nestedPtrOutput := ctx.newOutput(anyOutputType).(AnyOutput)
nestedPtrOutput.getState().resolve(&fileAssetOutput, true /*known*/, false /*secret*/, nil)
nestedNestedOutput := ctx.newOutput(anyOutputType).(AnyOutput)
nestedNestedOutput.getState().resolve(nestedOutput, true /*known*/, false /*secret*/, nil)
tests := []struct {
name string
input Input
@ -1277,6 +1342,114 @@ func TestOutputValueMarshallingNested(t *testing.T) {
}),
}),
},
{
name: "bucket object with file asset",
input: &BucketObjectArgs{
Source: NewFileAsset("foo.txt"),
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"source": resource.NewAssetProperty(&resource.Asset{
Path: "foo.txt",
}),
}),
},
{
name: "bucket object with file archive",
input: &BucketObjectArgs{
Source: NewFileArchive("bar.zip"),
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"source": resource.NewArchiveProperty(&resource.Archive{
Path: "bar.zip",
}),
}),
},
{
name: "bucket object with file asset output",
input: &BucketObjectArgs{
Source: fileAssetOutput,
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"source": resource.NewAssetProperty(&resource.Asset{
Path: "foo.txt",
}),
}),
},
{
name: "bucket object with file asset secret output",
input: &BucketObjectArgs{
Source: fileAssetSecretOutput,
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"source": resource.NewOutputProperty(resource.Output{
Element: resource.NewAssetProperty(&resource.Asset{
Path: "foo.txt",
}),
Known: true,
Secret: true,
}),
}),
},
{
name: "bucket object with file asset with deps",
input: &BucketObjectArgs{
Source: fileAssetOutputDeps,
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"source": resource.NewOutputProperty(resource.Output{
Element: resource.NewAssetProperty(&resource.Asset{
Path: "foo.txt",
}),
Known: true,
Dependencies: []resource.URN{"fakeURN"},
}),
}),
},
{
name: "resource",
input: &MyResourceArgs{
Res: NewResourceInput(newSimpleCustomResource(ctx, "fakeURN", "fakeID")),
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"res": resource.NewResourceReferenceProperty(resource.ResourceReference{
URN: "fakeURN",
ID: resource.NewStringProperty("fakeID"),
}),
}),
},
{
name: "nested output",
input: &MyNestedOutputArgs{
Nested: nestedOutput,
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"nested": resource.NewAssetProperty(&resource.Asset{
Path: "foo.txt",
}),
}),
},
{
name: "nested ptr output",
input: &MyNestedOutputArgs{
Nested: nestedPtrOutput,
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"nested": resource.NewAssetProperty(&resource.Asset{
Path: "foo.txt",
}),
}),
},
{
name: "nested nested output",
input: &MyNestedOutputArgs{
Nested: nestedNestedOutput,
},
expected: resource.NewObjectProperty(resource.PropertyMap{
"nested": resource.NewAssetProperty(&resource.Asset{
Path: "foo.txt",
}),
}),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {