[sdk/{go,dotnet] Unmarshal invalid assets. (#7579)

The two more strongly-typed Pulumi SDKs curently fail with an error
during unmarshaling when attempting to marshal a value that is not an
asset into an asset-typed location (e.g. an asset-typed resource
output property). While this behavior is reasonable on its face, it
gives rise to practical challenges when dealing with TF-provider-backed
resources that have asset-typed properties. When such a resource is
refreshed, the values of its asset-typed properties are replaced with
non-asset values, as the TF bridge can't currently create a resonable
stand-in asset value.

For example, consider an S3 bucket object:

```
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("my-bucket");
new aws.s3.BucketObject("my-object", {
    source: new pulumi.FileAsset("some/file"),
});
```

Upon creation, the value of the input property `source` will be a file
asset backed by the path `some/file`. The bridge will propagate this
value to the `source` output property; this propagation is safe because
the resource was just created and so the output property must have the
value that was passed by the program.

Now, let some actor apply out-of-band changes to the contents of the
bucket object s.t. the `source` property changes when the object is
refreshed. In that case, the `source` property will be a string value
which the bridge is unable to interpret as an asset. The next time the
Pulumi program is run, the Go or .NET SDK will attempt to deserialize
the string into an asset-typed property and will fail.

With these changes, the deserialization would not fail, and would
instead create an asset or archive value that will fail to marshal if
passed to another resource. Users can avoid these errors by not passing
asset or archive outputs to other resources/stack outputs.

These changes unblock users who are hitting
https://github.com/pulumi/pulumi-aws/issues/1521.
This commit is contained in:
Pat Gavlin 2021-07-21 13:40:36 -07:00 committed by GitHub
parent 81f87b0c6a
commit ece9f2fb30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 124 additions and 14 deletions

View file

@ -7,6 +7,9 @@
- [sdk/python] - Support for authoring resource methods in Python.
[#7555](https://github.com/pulumi/pulumi/pull/7555)
- [sdk/{go,dotnet}] - Admit non-asset/archive values when unmarshaling into assets and archives.
[#7579](https://github.com/pulumi/pulumi/pull/7579)
### Bug Fixes
- [sdk/go] - Fix target and replace options for the Automation API

View file

@ -739,6 +739,12 @@
A Pulumi program as an inline function (in process).
</summary>
</member>
<member name="M:Pulumi.Automation.PulumiFn.InvokeAsync(Pulumi.IRunner,System.Threading.CancellationToken)">
<summary>
Invoke the appropriate run function on the <see cref="T:Pulumi.IRunner"/> instance. The exit code returned
from the appropriate run function should be forwarded here as well.
</summary>
</member>
<member name="M:Pulumi.Automation.PulumiFn.Create(System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task{System.Collections.Generic.IDictionary{System.String,System.Object}}})">
<summary>
Creates an asynchronous inline (in process) pulumi program.
@ -803,6 +809,15 @@
<param name="serviceProvider">The service provider that will be used to resolve an instance of type <paramref name="stackType"/>.</param>
<param name="stackType">The stack type, which must derive from <see cref="T:Pulumi.Stack"/>.</param>
</member>
<member name="M:Pulumi.Automation.PulumiFnInline.InvokeAsync(Pulumi.IRunner,System.Threading.CancellationToken)">
<inheritdoc/>
</member>
<member name="M:Pulumi.Automation.PulumiFnServiceProvider.InvokeAsync(Pulumi.IRunner,System.Threading.CancellationToken)">
<inheritdoc/>
</member>
<member name="M:Pulumi.Automation.PulumiFn`1.InvokeAsync(Pulumi.IRunner,System.Threading.CancellationToken)">
<inheritdoc/>
</member>
<member name="T:Pulumi.Automation.RefreshOptions">
<summary>
Options controlling the behavior of an <see cref="M:Pulumi.Automation.WorkspaceStack.RefreshAsync(Pulumi.Automation.RefreshOptions,System.Threading.CancellationToken)"/> operation.

View file

@ -53,4 +53,11 @@ namespace Pulumi
{
}
}
sealed class InvalidArchive : Archive
{
public InvalidArchive() : base(Constants.ArchiveAssetsName, ImmutableDictionary<string, AssetOrArchive>.Empty)
{
}
}
}

View file

@ -48,4 +48,10 @@ namespace Pulumi
{
}
}
sealed class InvalidAsset : Asset
{
public InvalidAsset() : base(Constants.AssetTextName, "") {
}
}
}

View file

@ -98,14 +98,29 @@ namespace Pulumi.Serialization
if (targetType == typeof(object))
return (val, null);
if (targetType == typeof(Asset))
return TryEnsureType<Asset>(context, val);
if (targetType == typeof(Asset)) {
var (d, exception) = TryEnsureType<Asset>(context, val);
if (exception != null) {
d = new InvalidAsset();
}
return (d, null);
}
if (targetType == typeof(Archive))
return TryEnsureType<Archive>(context, val);
if (targetType == typeof(Archive)) {
var (d, exception) = TryEnsureType<Archive>(context, val);
if (exception != null) {
d = new InvalidArchive();
}
return (d, null);
}
if (targetType == typeof(AssetOrArchive))
return TryEnsureType<AssetOrArchive>(context, val);
if (targetType == typeof(AssetOrArchive)) {
var (d, exception) = TryEnsureType<AssetOrArchive>(context, val);
if (exception != null) {
d = new InvalidAsset();
}
return (d, null);
}
if (targetType == typeof(JsonElement))
return TryConvertJsonElement(context, val);

View file

@ -290,6 +290,11 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
Log.Debug($"Serialize property[{ctx}]: asset/archive={assetOrArchive.GetType().Name}");
}
if (assetOrArchive is InvalidAsset)
throw new InvalidOperationException("Cannot serialize invalid asset");
if (assetOrArchive is InvalidArchive)
throw new InvalidOperationException("Cannot serialize invalid archive");
var propName = assetOrArchive.PropName;
var value = await SerializeAsync(ctx + "." + propName, assetOrArchive.Value, keepResources).ConfigureAwait(false);

View file

@ -46,9 +46,10 @@ type Asset interface {
}
type asset struct {
path string
text string
uri string
path string
text string
uri string
invalid bool
}
// NewFileAsset creates an asset backed by a file and specified by that file's path.
@ -98,9 +99,10 @@ type Archive interface {
}
type archive struct {
assets map[string]interface{}
path string
uri string
assets map[string]interface{}
path string
uri string
invalid bool
}
// NewAssetArchive creates a new archive from an in-memory collection of named assets or other archives.

View file

@ -241,12 +241,19 @@ func marshalInputAndDetermineSecret(v interface{},
// Look for some well known types.
switch v := v.(type) {
case *asset:
if v.invalid {
return resource.PropertyValue{}, nil, false, fmt.Errorf("invalid asset")
}
return resource.NewAssetProperty(&resource.Asset{
Path: v.Path(),
Text: v.Text(),
URI: v.URI(),
}), deps, secret, nil
case *archive:
if v.invalid {
return resource.PropertyValue{}, nil, false, fmt.Errorf("invalid archive")
}
var assets map[string]interface{}
if as := v.Assets(); as != nil {
assets = make(map[string]interface{})
@ -662,7 +669,28 @@ func unmarshalOutput(ctx *Context, v resource.PropertyValue, dest reflect.Value)
dest.Set(result)
return secret, nil
case reflect.Interface:
if !anyType.Implements(dest.Type()) {
// Tolerate invalid asset or archive values.
typ := dest.Type()
switch typ {
case assetType:
_, secret, err := unmarshalPropertyValue(ctx, v)
if err != nil {
return false, err
}
asset := &asset{invalid: true}
dest.Set(reflect.ValueOf(asset))
return secret, nil
case archiveType:
_, secret, err := unmarshalPropertyValue(ctx, v)
if err != nil {
return false, err
}
archive := &archive{invalid: true}
dest.Set(reflect.ValueOf(archive))
return secret, nil
}
if !anyType.Implements(typ) {
return false, fmt.Errorf("cannot unmarshal into non-empty interface type %v", dest.Type())
}
@ -674,12 +702,12 @@ func unmarshalOutput(ctx *Context, v resource.PropertyValue, dest reflect.Value)
dest.Set(reflect.ValueOf(result))
return secret, nil
case reflect.Struct:
typ := dest.Type()
if !v.IsObject() {
return false, fmt.Errorf("expected a %v, got a %s", dest.Type(), v.TypeString())
}
obj := v.ObjectValue()
typ := dest.Type()
secret := false
for i := 0; i < typ.NumField(); i++ {
fieldV := dest.Field(i)

View file

@ -25,6 +25,7 @@ import (
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type simpleComponentResource struct {
@ -800,3 +801,31 @@ func TestRegisterResourceModule(t *testing.T) {
})
}
}
func TestInvalidAsset(t *testing.T) {
ctx, err := NewContext(context.Background(), RunInfo{})
assert.Nil(t, err)
var d Asset
_, err = unmarshalOutput(ctx, resource.NewStringProperty("foo"), reflect.ValueOf(&d).Elem())
require.NoError(t, err)
require.NotNil(t, d)
require.True(t, d.(*asset).invalid)
_, _, err = marshalInput(d, assetType, true)
assert.Error(t, err)
}
func TestInvalidArchive(t *testing.T) {
ctx, err := NewContext(context.Background(), RunInfo{})
assert.Nil(t, err)
var d Archive
_, err = unmarshalOutput(ctx, resource.NewStringProperty("foo"), reflect.ValueOf(&d).Elem())
require.NoError(t, err)
require.NotNil(t, d)
require.True(t, d.(*archive).invalid)
_, _, err = marshalInput(d, archiveType, true)
assert.Error(t, err)
}