Repository: pulumi/pulumi-terraform-bridge
Pull request #276 (2020-09-22 16:57:25)

Support provider-side preview.
Read the proposed state out of the diff.

pgavlin: These changes also include a local implementation of the field reader used to read values out of the Terraform diff.

The field reader implementations in the plugin SDK do not properly handle nested unknown values, instead treating such values as empty. The local implementation of the field reader correctly leaves nested unknown values as unknown.

A local implementation was chosen instead of an implementation in the plugin SDK fork so that it could be easily scoped to reading values from an `*InstanceDiff`, which only happens during previews. Naively changing the upstream field reader would affect all field reads, including those that happen during updates. This seems unacceptably risky. As an alternative, we could add a new implementation of the field reader in our fork, which may simplify any licensing issues.
pgavlin: I have verified that this fixes https://github.com/pulumi/pulumi-azure/issues/192.
github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/c996efe..0e07cca) with merge commit ccf101ec1386a16d78b23a5f06ff312af47732fc
github-actions[bot]: Diff for [pulumi-random](https://github.com/pulumi-bot/pulumi-random/compare/3997b58..3f2f958) with merge commit ccf101ec1386a16d78b23a5f06ff312af47732fc
github-actions[bot]: Diff for [pulumi-azure](https://github.com/pulumi-bot/pulumi-azure/compare/8a5df748..70a9ce9c) with merge commit ccf101ec1386a16d78b23a5f06ff312af47732fc
github-actions[bot]: Diff for [pulumi-aws](https://github.com/pulumi-bot/pulumi-aws/compare/c087a4d4d..83508877b) with merge commit ccf101ec1386a16d78b23a5f06ff312af47732fc
github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/9ba97d9..4c094b8) with merge commit 65adbbcf5d50b6c5987fb5403ce697b0cb6b945f
github-actions[bot]: Diff for [pulumi-random](https://github.com/pulumi-bot/pulumi-random/compare/bbe6738..9cbaadf) with merge commit 65adbbcf5d50b6c5987fb5403ce697b0cb6b945f
github-actions[bot]: Diff for [pulumi-azure](https://github.com/pulumi-bot/pulumi-azure/compare/499f6fe0..4f91658d) with merge commit 65adbbcf5d50b6c5987fb5403ce697b0cb6b945f
github-actions[bot]: Diff for [pulumi-gcp](https://github.com/pulumi-bot/pulumi-gcp/compare/b2fd8da9..651ddd3d) with merge commit ccf101ec1386a16d78b23a5f06ff312af47732fc
github-actions[bot]: Diff for [pulumi-aws](https://github.com/pulumi-bot/pulumi-aws/compare/acf0d9e33..8fc13e9bb) with merge commit 65adbbcf5d50b6c5987fb5403ce697b0cb6b945f
github-actions[bot]: Diff for [pulumi-gcp](https://github.com/pulumi-bot/pulumi-gcp/compare/69b58046..43e6db53) with merge commit 65adbbcf5d50b6c5987fb5403ce697b0cb6b945f
github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/0ec95c4..a808fb7) with merge commit 5e5606e42edc70b1b8da83511d52d55149dfe87d
github-actions[bot]: Diff for [pulumi-random](https://github.com/pulumi-bot/pulumi-random/compare/4c5f627..a9e43f5) with merge commit 5e5606e42edc70b1b8da83511d52d55149dfe87d
github-actions[bot]: Diff for [pulumi-azure](https://github.com/pulumi-bot/pulumi-azure/compare/5e6635cb..ae94b457) with merge commit 5e5606e42edc70b1b8da83511d52d55149dfe87d
github-actions[bot]: Diff for [pulumi-aws](https://github.com/pulumi-bot/pulumi-aws/compare/d14bd0c97..15ccfcf78) with merge commit 5e5606e42edc70b1b8da83511d52d55149dfe87d
github-actions[bot]: Diff for [pulumi-gcp](https://github.com/pulumi-bot/pulumi-gcp/compare/fe796b36..a25af9cf) with merge commit 5e5606e42edc70b1b8da83511d52d55149dfe87d

Patch:
diff --git a/.github/workflows/automate-provider-rollout.yml b/.github/workflows/automate-provider-rollout.yml
new file mode 100644
index 00000000..fc9dc6ac
--- /dev/null
+++ b/.github/workflows/automate-provider-rollout.yml
@@ -0,0 +1,50 @@
+env:
+  GITHUB_TOKEN: ${{ secrets.PULUMI_BOT_TOKEN }}
+
+name: Automate Downstream Rollout
+on:
+  repository_dispatch:
+    types:
+      - automate-rollout
+  workflow_dispatch:
+
+jobs:
+  downstream-provider:
+    name: Automate Downstream Pull Request
+    runs-on: ubuntu-latest
+    steps:
+      - name: Install Go
+        uses: actions/setup-go@v1
+        with:
+          go-version: 1.14.x
+      - name: Install Node.js
+        uses: actions/setup-node@v1
+        with:
+          node-version: '10.x'
+      - name: Install Python
+        uses: actions/setup-python@v1
+        with:
+          python-version: 3.6
+      - name: Install Pulumi CLI
+        uses: pulumi/action-install-pulumi-cli@releases/v1
+      - name: Install pulumictl
+        uses: jaxxstorm/action-install-gh-release@release/v1-alpha
+        with:
+          repo: pulumi/pulumictl
+      - name: Check out source code
+        uses: actions/checkout@master
+      - name: Generate Downstream Code
+        uses: pulumi/action-test-provider-downstream@317744132f8e9724dbb98ea2fbc5b212ea124594
+        env:
+          GOPROXY: "https://proxy.golang.org"
+        with:
+          downstream-name: pulumi-${{ matrix.provider_name }}
+          downstream-url: https://github.com/pulumi/pulumi-${{ matrix.provider_name }}
+          pulumi-bot-token: ${{ secrets.PULUMI_BOT_TOKEN }}
+          github-actions-token: ${{ secrets.GITHUB_TOKEN }}
+          use-provider-dir: true
+          open-pull-request: true
+    strategy:
+      matrix:
+        provider_name:
+          - keycloak
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 320fa072..3a6ea8e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,10 @@ CHANGELOG
 
 ## HEAD (Unreleased)
 
-- Add support for communicating with plugins over the  TF gRPC interface.
+- Add support for previewing `Create` and `Update` operations.
+  [#276](https://github.com/pulumi/pulumi-terraform-bridge/pull/276)
+
+- Add support for communicating with plugins over the TF gRPC interface.
   [#267](https://github.com/pulumi/pulumi-terraform-bridge/pull/267)
 
 - Refactor schematized types to support enums.
diff --git a/go.mod b/go.mod
index e705efe1..edbaf6de 100644
--- a/go.mod
+++ b/go.mod
@@ -33,7 +33,7 @@ require (
 	github.com/mitchellh/mapstructure v1.1.2
 	github.com/mitchellh/reflectwalk v1.0.1
 	github.com/pkg/errors v0.9.1
-	github.com/pulumi/pulumi/pkg/v2 v2.11.3-0.20201009201355-249140242ebb
+	github.com/pulumi/pulumi/pkg/v2 v2.12.1-0.20201019222817-89c956d18942
 	github.com/pulumi/pulumi/sdk/v2 v2.11.3-0.20201009201355-249140242ebb
 	github.com/spf13/afero v1.2.2
 	github.com/spf13/cobra v1.0.0
diff --git a/go.sum b/go.sum
index 41b0a593..722f56d6 100644
--- a/go.sum
+++ b/go.sum
@@ -398,6 +398,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
@@ -502,13 +504,9 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/pulumi/pulumi/pkg/v2 v2.10.2 h1:fmqbtsf+GR9DKJjB8dVYzu5b9J5C1ZPCKpmY+hGjSnc=
-github.com/pulumi/pulumi/pkg/v2 v2.10.2/go.mod h1:zQWe2D4tYJDeXNzSclqNmP8/SMSKOh8k22AbWg3+mVc=
-github.com/pulumi/pulumi/pkg/v2 v2.11.3-0.20201009201355-249140242ebb h1:xELQLO+B85/Rt7tGB+NcFvVYVbHg1Ftkd26rCvfA318=
-github.com/pulumi/pulumi/pkg/v2 v2.11.3-0.20201009201355-249140242ebb/go.mod h1:7NmgAy555Lk7v4Ddb6HF1BVAhdyT3012h47vz/hLD2k=
+github.com/pulumi/pulumi/pkg/v2 v2.12.1-0.20201019222817-89c956d18942 h1:IB34mt9friGXJGaRbZNwerIK9Oz0LfBruhbVEkJKDPg=
+github.com/pulumi/pulumi/pkg/v2 v2.12.1-0.20201019222817-89c956d18942/go.mod h1:a5zxh9WrYwWR/cDOhPMNFOUpJuwtyeFS+ZYJebehQ+c=
 github.com/pulumi/pulumi/sdk/v2 v2.2.1/go.mod h1:QNbWpL4gvf3X0lUFT7TXA2Jo1ff/Ti2l97AyFGYwvW4=
-github.com/pulumi/pulumi/sdk/v2 v2.10.2 h1:NPB5bs1b6ta/LEKQKh0hVIRUxczI4MTrEfAeymls5xY=
-github.com/pulumi/pulumi/sdk/v2 v2.10.2/go.mod h1:EED7KCDOohYIewUppsav5KHTFTmfYGqUFib1uRvYdWQ=
 github.com/pulumi/pulumi/sdk/v2 v2.11.3-0.20201009201355-249140242ebb h1:weg50wsLX2s+Uz6HyBkte0rZ1h+PNlBskElevjiS35I=
 github.com/pulumi/pulumi/sdk/v2 v2.11.3-0.20201009201355-249140242ebb/go.mod h1:WQ4WaHMA7mduVHAJi87iIqbWvqsuBUYccBiKK+FrayI=
 github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20200910230100-328eb4ff41df h1:kXSvfAz43o0D0gfxUqVCOP3NrfnGbptJBshVsqZlBqA=
diff --git a/pkg/tfbridge/provider.go b/pkg/tfbridge/provider.go
index 93a55bf6..30fe480f 100644
--- a/pkg/tfbridge/provider.go
+++ b/pkg/tfbridge/provider.go
@@ -549,7 +549,9 @@ func (p *Provider) Configure(ctx context.Context,
 		return nil, err
 	}
 
-	return &pulumirpc.ConfigureResponse{}, nil
+	return &pulumirpc.ConfigureResponse{
+		SupportsPreview: true,
+	}, nil
 }
 
 // Parse the TF error of a missing field:
@@ -789,20 +791,28 @@ func (p *Provider) Create(ctx context.Context, req *pulumirpc.CreateRequest) (*p
 		diff.SetTimeout(req.Timeout, shim.TimeoutCreate)
 	}
 
-	newstate, err := p.tf.Apply(res.TFName, nil, diff)
-	if newstate == nil {
-		if err == nil {
-			return nil, fmt.Errorf("expected non-nil error with nil state during Create of %s", urn)
+	var newstate shim.InstanceState
+	var reasons []string
+	if !req.GetPreview() {
+		newstate, err = p.tf.Apply(res.TFName, nil, diff)
+		if newstate == nil {
+			if err == nil {
+				return nil, fmt.Errorf("expected non-nil error with nil state during Create of %s", urn)
+			}
+			return nil, err
+		}
+		if newstate.ID() == "" {
+			return nil, fmt.Errorf("expected non-empty ID for new state during Create of %s", urn)
 		}
-		return nil, err
-	}
 
-	if newstate.ID() == "" {
-		return nil, fmt.Errorf("expected non-empty ID for new state during Create of %s", urn)
-	}
-	reasons := make([]string, 0)
-	if err != nil {
-		reasons = append(reasons, errors.Wrapf(err, "creating %s", urn).Error())
+		if err != nil {
+			reasons = append(reasons, errors.Wrapf(err, "creating %s", urn).Error())
+		}
+	} else {
+		newstate, err = diff.ProposedState(res.TF, nil)
+		if err != nil {
+			return nil, fmt.Errorf("internal error: failed to fetch proposed state during diff (%w)", err)
+		}
 	}
 
 	// Create the ID and property maps and return them.
@@ -954,23 +964,30 @@ func (p *Provider) Update(ctx context.Context, req *pulumirpc.UpdateRequest) (*p
 		diff.SetTimeout(req.Timeout, shim.TimeoutUpdate)
 	}
 
-	newstate, err := p.tf.Apply(res.TFName, state, diff)
-	if newstate == nil {
+	var newstate shim.InstanceState
+	var reasons []string
+	if !req.GetPreview() {
+		newstate, err = p.tf.Apply(res.TFName, state, diff)
+		if newstate == nil {
+			if err != nil {
+				return nil, err
+			}
+
+			return nil, fmt.Errorf("Resource provider reported that the resource did not exist while updating %s.\n\n"+
+				"This is usually a result of the resource having been deleted outside of Pulumi, and can often be "+
+				"fixed by running `pulumi refresh` before updating.", urn)
+		}
+		if newstate.ID() == "" {
+			return nil, fmt.Errorf("expected non-empty ID for new state during Update of %s", urn)
+		}
 		if err != nil {
-			return nil, err
+			reasons = append(reasons, errors.Wrapf(err, "updating %s", urn).Error())
+		}
+	} else {
+		newstate, err = diff.ProposedState(res.TF, state)
+		if err != nil {
+			return nil, fmt.Errorf("internal error: failed to fetch proposed state during diff (%w)", err)
 		}
-
-		return nil, fmt.Errorf("Resource provider reported that the resource did not exist while updating %s.\n\n"+
-			"This is usually a result of the resource having been deleted outside of Pulumi, and can often be "+
-			"fixed by running `pulumi refresh` before updating.", urn)
-	}
-
-	if newstate.ID() == "" {
-		return nil, fmt.Errorf("expected non-empty ID for new state during Update of %s", urn)
-	}
-	reasons := make([]string, 0)
-	if err != nil {
-		reasons = append(reasons, errors.Wrapf(err, "updating %s", urn).Error())
 	}
 
 	props, err := MakeTerraformResult(p.tf, newstate, res.TF.Schema(), res.Schema.Fields, assets, p.supportsSecrets)
diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go
index d570e6cc..fd561348 100644
--- a/pkg/tfbridge/schema.go
+++ b/pkg/tfbridge/schema.go
@@ -623,7 +623,10 @@ func MakeTerraformOutputs(p shim.Provider, outs map[string]interface{},
 		contract.Assert(name != "")
 
 		// Next perform a translation of the value accordingly.
-		result[name] = MakeTerraformOutput(p, value, tfi, psi, assets, rawNames, supportsSecrets)
+		out := MakeTerraformOutput(p, value, tfi, psi, assets, rawNames, supportsSecrets)
+		//if !out.IsNull() {
+		result[name] = out
+		//}
 	}
 
 	if glog.V(5) {
diff --git a/pkg/tfshim/sdk-v1/diff_reader.go b/pkg/tfshim/sdk-v1/diff_reader.go
new file mode 100644
index 00000000..58fa3c52
--- /dev/null
+++ b/pkg/tfshim/sdk-v1/diff_reader.go
@@ -0,0 +1,438 @@
+package sdkv1
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/terraform"
+	"github.com/mitchellh/mapstructure"
+)
+
+// diffFieldReader reads fields out of a diff structures.
+//
+// It also requires access to a Reader that reads fields from the structure
+// that the diff was derived from. This is usually the state. This is required
+// because a diff on its own doesn't have complete data about full objects
+// such as maps.
+//
+// The Source MUST be the data that the diff was derived from. If it isn't,
+// the behavior of this struct is undefined.
+//
+// Reading fields from a diffFieldReader is identical to reading from
+// Source except the diff will be applied to the end result.
+//
+// The "Exists" field on the result will be set to true if the complete
+// field exists whether its from the source, diff, or a combination of both.
+// It cannot be determined whether a retrieved value is composed of
+// diff elements.
+type diffFieldReader struct {
+	Diff   *terraform.InstanceDiff
+	Source schema.FieldReader
+	Schema map[string]*schema.Schema
+}
+
+func (r *diffFieldReader) ReadField(address []string) (schema.FieldReadResult, error) {
+	schemaList := schema.SchemasForFlatmapPath(strings.Join(address, "."), r.Schema)
+	if len(schemaList) == 0 {
+		return schema.FieldReadResult{}, nil
+	}
+
+	var res schema.FieldReadResult
+	var err error
+
+	sch := schemaList[len(schemaList)-1]
+	switch sch.Type {
+	case schema.TypeBool, schema.TypeInt, schema.TypeFloat, schema.TypeString:
+		res, err = r.readPrimitive(address, sch)
+	case schema.TypeList:
+		res, err = r.readListField(address, sch)
+	case schema.TypeMap:
+		res, err = r.readMap(address, sch)
+	case schema.TypeSet:
+		res, err = r.readSet(address, sch)
+	default:
+		res, err = r.readObjectField(address, sch.Elem.(map[string]*schema.Schema))
+	}
+
+	return res, err
+}
+
+// readListField is a generic method for reading a list field out of a
+// a FieldReader. It does this based on the assumption that there is a key
+// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc.
+// after that point.
+func (r *diffFieldReader) readListField(
+	addr []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	addrPadded := make([]string, len(addr)+1)
+	copy(addrPadded, addr)
+	addrPadded[len(addrPadded)-1] = "#"
+
+	// Get the number of elements in the list
+	countResult, err := r.ReadField(addrPadded)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+	if !countResult.Exists {
+		// No count, means we have no list
+		countResult.Value = 0
+	}
+
+	// If we have an empty list, then return an empty list
+	if countResult.Computed || countResult.Value.(int) == 0 {
+		return schema.FieldReadResult{
+			Value:    []interface{}{},
+			Exists:   countResult.Exists,
+			Computed: countResult.Computed,
+		}, nil
+	}
+
+	// Go through each count, and get the item value out of it
+	result := make([]interface{}, countResult.Value.(int))
+	for i := range result {
+		is := strconv.FormatInt(int64(i), 10)
+		addrPadded[len(addrPadded)-1] = is
+		rawResult, err := r.ReadField(addrPadded)
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if !rawResult.Exists {
+			// This should never happen, because by the time the data
+			// gets to the FieldReaders, all the defaults should be set by
+			// Schema.
+			rawResult.Value = nil
+		}
+		if rawResult.Computed {
+			result[i] = UnknownVariableValue
+		} else {
+			result[i] = rawResult.Value
+		}
+	}
+
+	return schema.FieldReadResult{
+		Value:  result,
+		Exists: true,
+	}, nil
+}
+
+// readObjectField is a generic method for reading objects out of FieldReaders
+// based on the assumption that building an address of []string{k, FIELD}
+// will result in the proper field data.
+func (r *diffFieldReader) readObjectField(
+	addr []string, sch map[string]*schema.Schema) (schema.FieldReadResult, error) {
+
+	result := make(map[string]interface{})
+	exists := false
+	for field, s := range sch {
+		addrRead := make([]string, len(addr), len(addr)+1)
+		copy(addrRead, addr)
+		addrRead = append(addrRead, field)
+		rawResult, err := r.ReadField(addrRead)
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if rawResult.Exists {
+			exists = true
+		}
+		if rawResult.Computed {
+			result[field] = UnknownVariableValue
+		} else {
+			result[field] = rawResult.ValueOrZero(s)
+		}
+	}
+
+	return schema.FieldReadResult{
+		Value:  result,
+		Exists: exists,
+	}, nil
+}
+
+func (r *diffFieldReader) readMap(
+	address []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	result := make(map[string]interface{})
+	resultSet := false
+
+	// First read the map from the underlying source
+	source, err := r.Source.ReadField(address)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+	if source.Exists {
+		// readMap may return a nil value, or an unknown value placeholder in
+		// some cases, causing the type assertion to panic if we don't assign the ok value
+		result, _ = source.Value.(map[string]interface{})
+		resultSet = true
+	}
+
+	// Next, read all the elements we have in our diff, and apply
+	// the diff to our result.
+	prefix := strings.Join(address, ".") + "."
+	for k, v := range r.Diff.Attributes {
+		if !strings.HasPrefix(k, prefix) {
+			continue
+		}
+		if strings.HasPrefix(k, prefix+"%") {
+			if v.NewComputed {
+				return schema.FieldReadResult{
+					Value:    result,
+					Exists:   true,
+					Computed: true,
+				}, nil
+			}
+
+			// Ignore the count field unless it is computed.
+			continue
+		}
+
+		resultSet = true
+
+		k = k[len(prefix):]
+		if v.NewRemoved {
+			delete(result, k)
+			continue
+		}
+		if v.NewComputed {
+			result[k] = UnknownVariableValue
+		} else {
+			result[k] = v.New
+		}
+	}
+
+	key := address[len(address)-1]
+	err = r.mapValuesToPrimitive(key, result, sch)
+	if err != nil {
+		return schema.FieldReadResult{}, nil
+	}
+
+	var resultVal interface{}
+	if resultSet {
+		resultVal = result
+	}
+
+	return schema.FieldReadResult{
+		Value:  resultVal,
+		Exists: resultSet,
+	}, nil
+}
+
+func (r *diffFieldReader) readPrimitive(
+	address []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	result, err := r.Source.ReadField(address)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+
+	attrD, ok := r.Diff.Attributes[strings.Join(address, ".")]
+	if !ok {
+		return result, nil
+	}
+
+	var resultVal string
+	if !attrD.NewComputed {
+		resultVal = attrD.New
+		if attrD.NewExtra != nil {
+			result.ValueProcessed = resultVal
+			if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil {
+				return schema.FieldReadResult{}, err
+			}
+		}
+	}
+
+	result.Computed = attrD.NewComputed
+	result.Exists = true
+	result.Value, err = r.stringToPrimitive(resultVal, sch)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+
+	return result, nil
+}
+
+func (r *diffFieldReader) readSet(
+	address []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	// copy address to ensure we don't modify the argument
+	address = append([]string(nil), address...)
+
+	prefix := strings.Join(address, ".") + "."
+
+	// Create the set that will be our result
+	set := sch.ZeroValue().(*schema.Set)
+
+	// Go through the map and find all the set items
+	for k, d := range r.Diff.Attributes {
+		if d.NewRemoved {
+			// If the field is removed, we always ignore it
+			continue
+		}
+		if !strings.HasPrefix(k, prefix) {
+			continue
+		}
+		if strings.HasSuffix(k, "#") {
+			// Ignore any count field
+			continue
+		}
+
+		// Split the key, since it might be a sub-object like "idx.field"
+		parts := strings.Split(k[len(prefix):], ".")
+		idx := parts[0]
+
+		raw, err := r.ReadField(append(address, idx))
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if !raw.Exists {
+			// This shouldn't happen because we just verified it does exist
+			panic("missing field in set: " + k + "." + idx)
+		}
+
+		// If any element of the set is computed, we must treat the whole set as computed.
+		if raw.Computed {
+			return schema.FieldReadResult{
+				Value:    set,
+				Exists:   true,
+				Computed: true,
+			}, nil
+		}
+
+		set.Add(raw.Value)
+	}
+
+	// Determine if the set "exists". It exists if there are items or if
+	// the diff explicitly wanted it empty.
+	exists := set.Len() > 0
+	if !exists {
+		// We could check if the diff value is "0" here but I think the
+		// existence of "#" on its own is enough to show it existed. This
+		// protects us in the future from the zero value changing from
+		// "0" to "" breaking us (if that were to happen).
+		if d, ok := r.Diff.Attributes[prefix+"#"]; ok {
+			if d.NewComputed {
+				return schema.FieldReadResult{
+					Value:    set,
+					Exists:   true,
+					Computed: true,
+				}, nil
+			}
+
+			exists = true
+		}
+	}
+
+	if !exists {
+		result, err := r.Source.ReadField(address)
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if result.Exists {
+			return result, nil
+		}
+	}
+
+	return schema.FieldReadResult{
+		Value:  set,
+		Exists: exists,
+	}, nil
+}
+
+func (r *diffFieldReader) getValueType(k string, sch *schema.Schema) (schema.ValueType, error) {
+	if sch.Elem == nil {
+		return schema.TypeString, nil
+	}
+	if vt, ok := sch.Elem.(schema.ValueType); ok {
+		return vt, nil
+	}
+
+	// If a Schema is provided to a Map, we use the Type of that schema
+	// as the type for each element in the Map.
+	if s, ok := sch.Elem.(*schema.Schema); ok {
+		return s.Type, nil
+	}
+
+	if _, ok := sch.Elem.(*schema.Resource); ok {
+		// TODO: We don't actually support this (yet)
+		// but silently pass the validation, until we decide
+		// how to handle nested structures in maps
+		return schema.TypeString, nil
+	}
+	return 0, fmt.Errorf("%s: unexpected map value type: %#v", k, sch.Elem)
+}
+
+// convert map values to the proper primitive type based on schema.Elem
+func (r *diffFieldReader) mapValuesToPrimitive(k string, m map[string]interface{}, sch *schema.Schema) error {
+	elemType, err := r.getValueType(k, sch)
+	if err != nil {
+		return err
+	}
+
+	switch elemType {
+	case schema.TypeInt, schema.TypeFloat, schema.TypeBool:
+		for k, v := range m {
+			vs, ok := v.(string)
+			if !ok || v == UnknownVariableValue {
+				continue
+			}
+
+			v, err := r.stringToPrimitive(vs, &schema.Schema{Type: elemType})
+			if err != nil {
+				return err
+			}
+
+			m[k] = v
+		}
+	}
+	return nil
+}
+
+func (r *diffFieldReader) stringToPrimitive(
+	value string, sch *schema.Schema) (interface{}, error) {
+	var returnVal interface{}
+	switch sch.Type {
+	case schema.TypeBool:
+		if value == "" {
+			returnVal = false
+			break
+		}
+
+		v, err := strconv.ParseBool(value)
+		if err != nil {
+			return nil, err
+		}
+
+		returnVal = v
+	case schema.TypeFloat:
+		if value == "" {
+			returnVal = 0.0
+			break
+		}
+
+		v, err := strconv.ParseFloat(value, 64)
+		if err != nil {
+			return nil, err
+		}
+
+		returnVal = v
+	case schema.TypeInt:
+		if value == "" {
+			returnVal = 0
+			break
+		}
+
+		v, err := strconv.ParseInt(value, 0, 0)
+		if err != nil {
+			return nil, err
+		}
+
+		returnVal = int(v)
+	case schema.TypeString:
+		returnVal = value
+	default:
+		panic(fmt.Errorf("Unknown type: %v", sch.Type))
+	}
+
+	return returnVal, nil
+}
diff --git a/pkg/tfshim/sdk-v1/instance_diff.go b/pkg/tfshim/sdk-v1/instance_diff.go
index 6d209d8d..e2e94473 100644
--- a/pkg/tfshim/sdk-v1/instance_diff.go
+++ b/pkg/tfshim/sdk-v1/instance_diff.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/terraform"
+
 	shim "github.com/pulumi/pulumi-terraform-bridge/v2/pkg/tfshim"
 )
 
@@ -54,6 +55,20 @@ func (d v1InstanceDiff) Attributes() map[string]shim.ResourceAttrDiff {
 	return m
 }
 
+func (d v1InstanceDiff) ProposedState(res shim.Resource, priorState shim.InstanceState) (shim.InstanceState, error) {
+	var prior *terraform.InstanceState
+	if priorState != nil {
+		prior = priorState.(v1InstanceState).tf
+	} else {
+		prior = &terraform.InstanceState{
+			Attributes: map[string]string{},
+			Meta:       map[string]interface{}{},
+		}
+	}
+
+	return v1InstanceState{tf: prior, diff: d.tf}, nil
+}
+
 func (d v1InstanceDiff) Destroy() bool {
 	return d.tf.Destroy
 }
diff --git a/pkg/tfshim/sdk-v1/instance_state.go b/pkg/tfshim/sdk-v1/instance_state.go
index 52d79d46..49ddbd91 100644
--- a/pkg/tfshim/sdk-v1/instance_state.go
+++ b/pkg/tfshim/sdk-v1/instance_state.go
@@ -11,11 +11,12 @@ import (
 var _ = shim.InstanceState(v1InstanceState{})
 
 type v1InstanceState struct {
-	tf *terraform.InstanceState
+	tf   *terraform.InstanceState
+	diff *terraform.InstanceDiff
 }
 
 func NewInstanceState(s *terraform.InstanceState) shim.InstanceState {
-	return v1InstanceState{s}
+	return v1InstanceState{s, nil}
 }
 
 func IsInstanceState(s shim.InstanceState) (*terraform.InstanceState, bool) {
@@ -35,16 +36,28 @@ func (s v1InstanceState) ID() string {
 
 func (s v1InstanceState) Object(sch shim.SchemaMap) (map[string]interface{}, error) {
 	obj := make(map[string]interface{})
+
+	schemaMap := map[string]*schema.Schema(sch.(v1SchemaMap))
+
 	attrs := s.tf.Attributes
 
-	reader := &schema.MapFieldReader{
-		Schema: map[string]*schema.Schema(sch.(v1SchemaMap)),
+	var reader schema.FieldReader = &schema.MapFieldReader{
+		Schema: schemaMap,
 		Map:    schema.BasicMapReader(attrs),
 	}
 
+	// If this is a state + a diff, use a diff reader rather than a map reader.
+	if s.diff != nil {
+		reader = &diffFieldReader{
+			Diff:   s.diff,
+			Schema: schemaMap,
+			Source: reader,
+		}
+	}
+
 	// Read each top-level field out of the attributes.
 	keys := make(map[string]bool)
-	for key := range attrs {
+	readAttributeField := func(key string) error {
 		// Pull the top-level field out of this attribute key. If we've already read the top-level field, skip this
 		// key.
 		dot := strings.Index(key, ".")
@@ -52,18 +65,32 @@ func (s v1InstanceState) Object(sch shim.SchemaMap) (map[string]interface{}, err
 			key = key[:dot]
 		}
 		if _, ok := keys[key]; ok {
-			continue
+			return nil
 		}
 		keys[key] = true
 
 		// Read the top-level attribute for this key.
 		res, err := reader.ReadField([]string{key})
 		if err != nil {
-			return nil, err
+			return err
 		}
-		if res.Value != nil {
+		if res.Value != nil && !res.Computed {
 			obj[key] = res.Value
 		}
+		return nil
+	}
+
+	for key := range attrs {
+		if err := readAttributeField(key); err != nil {
+			return nil, err
+		}
+	}
+	if s.diff != nil {
+		for key := range s.diff.Attributes {
+			if err := readAttributeField(key); err != nil {
+				return nil, err
+			}
+		}
 	}
 
 	// Populate the "id" property if it is not set. Most schemas do not include this property, and leaving it out
diff --git a/pkg/tfshim/sdk-v1/provider.go b/pkg/tfshim/sdk-v1/provider.go
index 7dfafd17..6aa786cb 100644
--- a/pkg/tfshim/sdk-v1/provider.go
+++ b/pkg/tfshim/sdk-v1/provider.go
@@ -31,7 +31,7 @@ func stateToShim(s *terraform.InstanceState) shim.InstanceState {
 	if s == nil {
 		return nil
 	}
-	return v1InstanceState{s}
+	return v1InstanceState{s, nil}
 }
 
 func diffFromShim(d shim.InstanceDiff) *terraform.InstanceDiff {
diff --git a/pkg/tfshim/sdk-v1/resource.go b/pkg/tfshim/sdk-v1/resource.go
index df05ad52..d0f4cadd 100644
--- a/pkg/tfshim/sdk-v1/resource.go
+++ b/pkg/tfshim/sdk-v1/resource.go
@@ -49,7 +49,7 @@ func (r v1Resource) Importer() shim.ImportFunc {
 					"as a bug in the Pulumi provider repository.", id)
 			}
 			if s.Attributes != nil {
-				results[i] = v1InstanceState{s}
+				results[i] = v1InstanceState{s, nil}
 			}
 		}
 		return results, nil
@@ -97,7 +97,7 @@ func (r v1Resource) InstanceState(id string, object, meta map[string]interface{}
 		ID:         id,
 		Attributes: attributes,
 		Meta:       meta,
-	}}, nil
+	}, nil}, nil
 }
 
 func (r v1Resource) DecodeTimeouts(config shim.ResourceConfig) (*shim.ResourceTimeout, error) {
diff --git a/pkg/tfshim/sdk-v2/diff_reader.go b/pkg/tfshim/sdk-v2/diff_reader.go
new file mode 100644
index 00000000..ed838650
--- /dev/null
+++ b/pkg/tfshim/sdk-v2/diff_reader.go
@@ -0,0 +1,566 @@
+package sdkv2
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/mitchellh/mapstructure"
+)
+
+// diffFieldReader reads fields out of a diff structures.
+//
+// It also requires access to a Reader that reads fields from the structure
+// that the diff was derived from. This is usually the state. This is required
+// because a diff on its own doesn't have complete data about full objects
+// such as maps.
+//
+// The Source MUST be the data that the diff was derived from. If it isn't,
+// the behavior of this struct is undefined.
+//
+// Reading fields from a diffFieldReader is identical to reading from
+// Source except the diff will be applied to the end result.
+//
+// The "Exists" field on the result will be set to true if the complete
+// field exists whether its from the source, diff, or a combination of both.
+// It cannot be determined whether a retrieved value is composed of
+// diff elements.
+type diffFieldReader struct {
+	Diff   *terraform.InstanceDiff
+	Source schema.FieldReader
+	Schema map[string]*schema.Schema
+}
+
+func (r *diffFieldReader) ReadField(address []string) (schema.FieldReadResult, error) {
+	schemaList := r.addrToSchema(address, r.Schema)
+	if len(schemaList) == 0 {
+		return schema.FieldReadResult{}, nil
+	}
+
+	var res schema.FieldReadResult
+	var err error
+
+	sch := schemaList[len(schemaList)-1]
+	switch sch.Type {
+	case schema.TypeBool, schema.TypeInt, schema.TypeFloat, schema.TypeString:
+		res, err = r.readPrimitive(address, sch)
+	case schema.TypeList:
+		res, err = r.readListField(address, sch)
+	case schema.TypeMap:
+		res, err = r.readMap(address, sch)
+	case schema.TypeSet:
+		res, err = r.readSet(address, sch)
+	default:
+		res, err = r.readObjectField(address, sch.Elem.(map[string]*schema.Schema))
+	}
+
+	return res, err
+}
+
+// addrToSchema finds the final element schema for the given address
+// and the given schema. It returns all the schemas that led to the final
+// schema. These are in order of the address (out to in).
+func (r *diffFieldReader) addrToSchema(addr []string, schemaMap map[string]*schema.Schema) []*schema.Schema {
+	// NOTE: this is a total hack, and is brittle with respect to the actual definition in the TF plugin SDK.
+	const typeObject = schema.TypeSet + 1
+
+	current := &schema.Schema{
+		Type: typeObject,
+		Elem: schemaMap,
+	}
+
+	// If we aren't given an address, then the user is requesting the
+	// full object, so we return the special value which is the full object.
+	if len(addr) == 0 {
+		return []*schema.Schema{current}
+	}
+
+	result := make([]*schema.Schema, 0, len(addr))
+	for len(addr) > 0 {
+		k := addr[0]
+		addr = addr[1:]
+
+	REPEAT:
+		// We want to trim off the first "typeObject" since its not a
+		// real lookup that people do. i.e. []string{"foo"} in a structure
+		// isn't {typeObject, typeString}, its just a {typeString}.
+		if len(result) > 0 || current.Type != typeObject {
+			result = append(result, current)
+		}
+
+		switch t := current.Type; t {
+		case schema.TypeBool, schema.TypeInt, schema.TypeFloat, schema.TypeString:
+			if len(addr) > 0 {
+				return nil
+			}
+		case schema.TypeList, schema.TypeSet:
+			isIndex := len(addr) > 0 && addr[0] == "#"
+
+			switch v := current.Elem.(type) {
+			case *schema.Resource:
+				current = &schema.Schema{
+					Type: typeObject,
+					Elem: v.Schema,
+				}
+			case *schema.Schema:
+				current = v
+			case schema.ValueType:
+				current = &schema.Schema{Type: v}
+			default:
+				// we may not know the Elem type and are just looking for the
+				// index
+				if isIndex {
+					break
+				}
+
+				if len(addr) == 0 {
+					// we've processed the address, so return what we've
+					// collected
+					return result
+				}
+
+				if len(addr) == 1 {
+					if _, err := strconv.Atoi(addr[0]); err == nil {
+						// we're indexing a value without a schema. This can
+						// happen if the list is nested in another schema type.
+						// Default to a TypeString like we do with a map
+						current = &schema.Schema{Type: schema.TypeString}
+						break
+					}
+				}
+
+				return nil
+			}
+
+			// If we only have one more thing and the next thing
+			// is a #, then we're accessing the index which is always
+			// an int.
+			if isIndex {
+				current = &schema.Schema{Type: schema.TypeInt}
+				break
+			}
+
+		case schema.TypeMap:
+			if len(addr) > 0 {
+				switch v := current.Elem.(type) {
+				case schema.ValueType:
+					current = &schema.Schema{Type: v}
+				case *schema.Schema:
+					current, _ = current.Elem.(*schema.Schema)
+				default:
+					// maps default to string values. This is all we can have
+					// if this is nested in another list or map.
+					current = &schema.Schema{Type: schema.TypeString}
+				}
+			}
+		case typeObject:
+			// If we're already in the object, then we want to handle Sets
+			// and Lists specially. Basically, their next key is the lookup
+			// key (the set value or the list element). For these scenarios,
+			// we just want to skip it and move to the next element if there
+			// is one.
+			if len(result) > 0 {
+				lastType := result[len(result)-2].Type
+				if lastType == schema.TypeSet || lastType == schema.TypeList {
+					if len(addr) == 0 {
+						break
+					}
+
+					k = addr[0]
+					addr = addr[1:]
+				}
+			}
+
+			m := current.Elem.(map[string]*schema.Schema)
+			val, ok := m[k]
+			if !ok {
+				return nil
+			}
+
+			current = val
+			goto REPEAT
+		}
+	}
+
+	return result
+}
+
+// readListField is a generic method for reading a list field out of a
+// a FieldReader. It does this based on the assumption that there is a key
+// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc.
+// after that point.
+func (r *diffFieldReader) readListField(
+	addr []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	addrPadded := make([]string, len(addr)+1)
+	copy(addrPadded, addr)
+	addrPadded[len(addrPadded)-1] = "#"
+
+	// Get the number of elements in the list
+	countResult, err := r.ReadField(addrPadded)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+	if !countResult.Exists {
+		// No count, means we have no list
+		countResult.Value = 0
+	}
+
+	// If we have an empty list, then return an empty list
+	if countResult.Computed || countResult.Value.(int) == 0 {
+		return schema.FieldReadResult{
+			Value:    []interface{}{},
+			Exists:   countResult.Exists,
+			Computed: countResult.Computed,
+		}, nil
+	}
+
+	// Go through each count, and get the item value out of it
+	result := make([]interface{}, countResult.Value.(int))
+	for i := range result {
+		is := strconv.FormatInt(int64(i), 10)
+		addrPadded[len(addrPadded)-1] = is
+		rawResult, err := r.ReadField(addrPadded)
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if !rawResult.Exists {
+			// This should never happen, because by the time the data
+			// gets to the FieldReaders, all the defaults should be set by
+			// Schema.
+			rawResult.Value = nil
+		}
+		if rawResult.Computed {
+			result[i] = UnknownVariableValue
+		} else {
+			result[i] = rawResult.Value
+		}
+	}
+
+	return schema.FieldReadResult{
+		Value:  result,
+		Exists: true,
+	}, nil
+}
+
+// readObjectField is a generic method for reading objects out of FieldReaders
+// based on the assumption that building an address of []string{k, FIELD}
+// will result in the proper field data.
+func (r *diffFieldReader) readObjectField(
+	addr []string, sch map[string]*schema.Schema) (schema.FieldReadResult, error) {
+
+	result := make(map[string]interface{})
+	exists := false
+	for field, s := range sch {
+		addrRead := make([]string, len(addr), len(addr)+1)
+		copy(addrRead, addr)
+		addrRead = append(addrRead, field)
+		rawResult, err := r.ReadField(addrRead)
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if rawResult.Exists {
+			exists = true
+		}
+		if rawResult.Computed {
+			result[field] = UnknownVariableValue
+		} else {
+			result[field] = rawResult.ValueOrZero(s)
+		}
+	}
+
+	return schema.FieldReadResult{
+		Value:  result,
+		Exists: exists,
+	}, nil
+}
+
+func (r *diffFieldReader) readMap(
+	address []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	result := make(map[string]interface{})
+	resultSet := false
+
+	// First read the map from the underlying source
+	source, err := r.Source.ReadField(address)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+	if source.Exists {
+		// readMap may return a nil value, or an unknown value placeholder in
+		// some cases, causing the type assertion to panic if we don't assign the ok value
+		result, _ = source.Value.(map[string]interface{})
+		resultSet = true
+	}
+
+	// Next, read all the elements we have in our diff, and apply
+	// the diff to our result.
+	prefix := strings.Join(address, ".") + "."
+	for k, v := range r.Diff.Attributes {
+		if !strings.HasPrefix(k, prefix) {
+			continue
+		}
+		if strings.HasPrefix(k, prefix+"%") {
+			if v.NewComputed {
+				return schema.FieldReadResult{
+					Value:    result,
+					Exists:   true,
+					Computed: true,
+				}, nil
+			}
+
+			// Ignore the count field unless it is computed.
+			continue
+		}
+
+		resultSet = true
+
+		k = k[len(prefix):]
+		if v.NewRemoved {
+			delete(result, k)
+			continue
+		}
+		if v.NewComputed {
+			result[k] = UnknownVariableValue
+		} else {
+			result[k] = v.New
+		}
+	}
+
+	key := address[len(address)-1]
+	err = r.mapValuesToPrimitive(key, result, sch)
+	if err != nil {
+		return schema.FieldReadResult{}, nil
+	}
+
+	var resultVal interface{}
+	if resultSet {
+		resultVal = result
+	}
+
+	return schema.FieldReadResult{
+		Value:  resultVal,
+		Exists: resultSet,
+	}, nil
+}
+
+func (r *diffFieldReader) readPrimitive(
+	address []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	result, err := r.Source.ReadField(address)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+
+	attrD, ok := r.Diff.Attributes[strings.Join(address, ".")]
+	if !ok {
+		return result, nil
+	}
+
+	var resultVal string
+	if !attrD.NewComputed {
+		resultVal = attrD.New
+		if attrD.NewExtra != nil {
+			result.ValueProcessed = resultVal
+			if err := mapstructure.WeakDecode(attrD.NewExtra, &resultVal); err != nil {
+				return schema.FieldReadResult{}, err
+			}
+		}
+	}
+
+	result.Computed = attrD.NewComputed
+	result.Exists = true
+	result.Value, err = r.stringToPrimitive(resultVal, sch)
+	if err != nil {
+		return schema.FieldReadResult{}, err
+	}
+
+	return result, nil
+}
+
+func (r *diffFieldReader) readSet(
+	address []string, sch *schema.Schema) (schema.FieldReadResult, error) {
+
+	// copy address to ensure we don't modify the argument
+	address = append([]string(nil), address...)
+
+	prefix := strings.Join(address, ".") + "."
+
+	// Create the set that will be our result
+	set := sch.ZeroValue().(*schema.Set)
+
+	// Go through the map and find all the set items
+	for k, d := range r.Diff.Attributes {
+		if d.NewRemoved {
+			// If the field is removed, we always ignore it
+			continue
+		}
+		if !strings.HasPrefix(k, prefix) {
+			continue
+		}
+		if strings.HasSuffix(k, "#") {
+			// Ignore any count field
+			continue
+		}
+
+		// Split the key, since it might be a sub-object like "idx.field"
+		parts := strings.Split(k[len(prefix):], ".")
+		idx := parts[0]
+
+		raw, err := r.ReadField(append(address, idx))
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if !raw.Exists {
+			// This shouldn't happen because we just verified it does exist
+			panic("missing field in set: " + k + "." + idx)
+		}
+
+		// If any element of the set is computed, we must treat the whole set as computed.
+		if raw.Computed {
+			return schema.FieldReadResult{
+				Value:    set,
+				Exists:   true,
+				Computed: true,
+			}, nil
+		}
+
+		set.Add(raw.Value)
+	}
+
+	// Determine if the set "exists". It exists if there are items or if
+	// the diff explicitly wanted it empty.
+	exists := set.Len() > 0
+	if !exists {
+		// We could check if the diff value is "0" here but I think the
+		// existence of "#" on its own is enough to show it existed. This
+		// protects us in the future from the zero value changing from
+		// "0" to "" breaking us (if that were to happen).
+		if d, ok := r.Diff.Attributes[prefix+"#"]; ok {
+			if d.NewComputed {
+				return schema.FieldReadResult{
+					Value:    set,
+					Exists:   true,
+					Computed: true,
+				}, nil
+			}
+
+			exists = true
+		}
+	}
+
+	if !exists {
+		result, err := r.Source.ReadField(address)
+		if err != nil {
+			return schema.FieldReadResult{}, err
+		}
+		if result.Exists {
+			return result, nil
+		}
+	}
+
+	return schema.FieldReadResult{
+		Value:  set,
+		Exists: exists,
+	}, nil
+}
+
+func (r *diffFieldReader) getValueType(k string, sch *schema.Schema) (schema.ValueType, error) {
+	if sch.Elem == nil {
+		return schema.TypeString, nil
+	}
+	if vt, ok := sch.Elem.(schema.ValueType); ok {
+		return vt, nil
+	}
+
+	// If a Schema is provided to a Map, we use the Type of that schema
+	// as the type for each element in the Map.
+	if s, ok := sch.Elem.(*schema.Schema); ok {
+		return s.Type, nil
+	}
+
+	if _, ok := sch.Elem.(*schema.Resource); ok {
+		// TODO: We don't actually support this (yet)
+		// but silently pass the validation, until we decide
+		// how to handle nested structures in maps
+		return schema.TypeString, nil
+	}
+	return 0, fmt.Errorf("%s: unexpected map value type: %#v", k, sch.Elem)
+}
+
+// convert map values to the proper primitive type based on schema.Elem
+func (r *diffFieldReader) mapValuesToPrimitive(k string, m map[string]interface{}, sch *schema.Schema) error {
+	elemType, err := r.getValueType(k, sch)
+	if err != nil {
+		return err
+	}
+
+	switch elemType {
+	case schema.TypeInt, schema.TypeFloat, schema.TypeBool:
+		for k, v := range m {
+			vs, ok := v.(string)
+			if !ok || v == UnknownVariableValue {
+				continue
+			}
+
+			v, err := r.stringToPrimitive(vs, &schema.Schema{Type: elemType})
+			if err != nil {
+				return err
+			}
+
+			m[k] = v
+		}
+	}
+	return nil
+}
+
+func (r *diffFieldReader) stringToPrimitive(
+	value string, sch *schema.Schema) (interface{}, error) {
+	var returnVal interface{}
+	switch sch.Type {
+	case schema.TypeBool:
+		if value == "" {
+			returnVal = false
+			break
+		}
+
+		v, err := strconv.ParseBool(value)
+		if err != nil {
+			return nil, err
+		}
+
+		returnVal = v
+	case schema.TypeFloat:
+		if value == "" {
+			returnVal = 0.0
+			break
+		}
+
+		v, err := strconv.ParseFloat(value, 64)
+		if err != nil {
+			return nil, err
+		}
+
+		returnVal = v
+	case schema.TypeInt:
+		if value == "" {
+			returnVal = 0
+			break
+		}
+
+		v, err := strconv.ParseInt(value, 0, 0)
+		if err != nil {
+			return nil, err
+		}
+
+		returnVal = int(v)
+	case schema.TypeString:
+		returnVal = value
+	default:
+		panic(fmt.Errorf("Unknown type: %v", sch.Type))
+	}
+
+	return returnVal, nil
+}
diff --git a/pkg/tfshim/sdk-v2/instance_diff.go b/pkg/tfshim/sdk-v2/instance_diff.go
index 0638e6f3..80d4888d 100644
--- a/pkg/tfshim/sdk-v2/instance_diff.go
+++ b/pkg/tfshim/sdk-v2/instance_diff.go
@@ -44,6 +44,20 @@ func (d v2InstanceDiff) Attributes() map[string]shim.ResourceAttrDiff {
 	return m
 }
 
+func (d v2InstanceDiff) ProposedState(res shim.Resource, priorState shim.InstanceState) (shim.InstanceState, error) {
+	var prior *terraform.InstanceState
+	if priorState != nil {
+		prior = priorState.(v2InstanceState).tf
+	} else {
+		prior = &terraform.InstanceState{
+			Attributes: map[string]string{},
+			Meta:       map[string]interface{}{},
+		}
+	}
+
+	return v2InstanceState{tf: prior, diff: d.tf}, nil
+}
+
 func (d v2InstanceDiff) Destroy() bool {
 	return d.tf.Destroy
 }
diff --git a/pkg/tfshim/sdk-v2/instance_state.go b/pkg/tfshim/sdk-v2/instance_state.go
index 1611cb46..886f7640 100644
--- a/pkg/tfshim/sdk-v2/instance_state.go
+++ b/pkg/tfshim/sdk-v2/instance_state.go
@@ -11,11 +11,12 @@ import (
 var _ = shim.InstanceState(v2InstanceState{})
 
 type v2InstanceState struct {
-	tf *terraform.InstanceState
+	tf   *terraform.InstanceState
+	diff *terraform.InstanceDiff
 }
 
 func NewInstanceState(s *terraform.InstanceState) shim.InstanceState {
-	return v2InstanceState{s}
+	return v2InstanceState{s, nil}
 }
 
 func IsInstanceState(s shim.InstanceState) (*terraform.InstanceState, bool) {
@@ -35,16 +36,28 @@ func (s v2InstanceState) ID() string {
 
 func (s v2InstanceState) Object(sch shim.SchemaMap) (map[string]interface{}, error) {
 	obj := make(map[string]interface{})
+
+	schemaMap := map[string]*schema.Schema(sch.(v2SchemaMap))
+
 	attrs := s.tf.Attributes
 
-	reader := &schema.MapFieldReader{
-		Schema: map[string]*schema.Schema(sch.(v2SchemaMap)),
+	var reader schema.FieldReader = &schema.MapFieldReader{
+		Schema: schemaMap,
 		Map:    schema.BasicMapReader(attrs),
 	}
 
+	// If this is a state + a diff, use a diff reader rather than a map reader.
+	if s.diff != nil {
+		reader = &diffFieldReader{
+			Diff:   s.diff,
+			Schema: schemaMap,
+			Source: reader,
+		}
+	}
+
 	// Read each top-level field out of the attributes.
 	keys := make(map[string]bool)
-	for key := range attrs {
+	readAttributeField := func(key string) error {
 		// Pull the top-level field out of this attribute key. If we've already read the top-level field, skip this
 		// key.
 		dot := strings.Index(key, ".")
@@ -52,18 +65,32 @@ func (s v2InstanceState) Object(sch shim.SchemaMap) (map[string]interface{}, err
 			key = key[:dot]
 		}
 		if _, ok := keys[key]; ok {
-			continue
+			return nil
 		}
 		keys[key] = true
 
 		// Read the top-level attribute for this key.
 		res, err := reader.ReadField([]string{key})
 		if err != nil {
-			return nil, err
+			return err
 		}
-		if res.Value != nil {
+		if res.Value != nil && !res.Computed {
 			obj[key] = res.Value
 		}
+		return nil
+	}
+
+	for key := range attrs {
+		if err := readAttributeField(key); err != nil {
+			return nil, err
+		}
+	}
+	if s.diff != nil {
+		for key := range s.diff.Attributes {
+			if err := readAttributeField(key); err != nil {
+				return nil, err
+			}
+		}
 	}
 
 	// Populate the "id" property if it is not set. Most schemas do not include this property, and leaving it out
diff --git a/pkg/tfshim/sdk-v2/provider.go b/pkg/tfshim/sdk-v2/provider.go
index 2129b353..91f516ea 100644
--- a/pkg/tfshim/sdk-v2/provider.go
+++ b/pkg/tfshim/sdk-v2/provider.go
@@ -31,7 +31,7 @@ func stateToShim(s *terraform.InstanceState) shim.InstanceState {
 	if s == nil {
 		return nil
 	}
-	return v2InstanceState{s}
+	return v2InstanceState{s, nil}
 }
 
 func diffFromShim(d shim.InstanceDiff) *terraform.InstanceDiff {
diff --git a/pkg/tfshim/sdk-v2/resource.go b/pkg/tfshim/sdk-v2/resource.go
index 1a6115e7..c02afac9 100644
--- a/pkg/tfshim/sdk-v2/resource.go
+++ b/pkg/tfshim/sdk-v2/resource.go
@@ -58,7 +58,7 @@ func (r v2Resource) Importer() shim.ImportFunc {
 					"as a bug in the Pulumi provider repository.", id)
 			}
 			if s.Attributes != nil {
-				results[i] = v2InstanceState{s}
+				results[i] = v2InstanceState{s, nil}
 			}
 		}
 		return results, nil
@@ -106,7 +106,7 @@ func (r v2Resource) InstanceState(id string, object, meta map[string]interface{}
 		ID:         id,
 		Attributes: attributes,
 		Meta:       meta,
-	}}, nil
+	}, nil}, nil
 }
 
 func (r v2Resource) DecodeTimeouts(config shim.ResourceConfig) (*shim.ResourceTimeout, error) {
diff --git a/pkg/tfshim/shim.go b/pkg/tfshim/shim.go
index fc203857..417b9734 100644
--- a/pkg/tfshim/shim.go
+++ b/pkg/tfshim/shim.go
@@ -37,7 +37,7 @@ type ResourceAttrDiff struct {
 type InstanceDiff interface {
 	Attribute(key string) *ResourceAttrDiff
 	Attributes() map[string]ResourceAttrDiff
-
+	ProposedState(res Resource, priorState InstanceState) (InstanceState, error)
 	Destroy() bool
 	RequiresNew() bool
 
diff --git a/pkg/tfshim/tfplugin5/instance_diff.go b/pkg/tfshim/tfplugin5/instance_diff.go
index 83f4e527..bc7cea05 100644
--- a/pkg/tfshim/tfplugin5/instance_diff.go
+++ b/pkg/tfshim/tfplugin5/instance_diff.go
@@ -51,6 +51,25 @@ func (d *instanceDiff) Attributes() map[string]shim.ResourceAttrDiff {
 	return d.attributes
 }
 
+func (d *instanceDiff) ProposedState(res shim.Resource, priorState shim.InstanceState) (shim.InstanceState, error) {
+	plannedObject, err := ctyToGo(d.planned)
+	if err != nil {
+		return nil, err
+	}
+
+	var id string
+	if priorState != nil {
+		id = priorState.ID()
+	}
+
+	return &instanceState{
+		resourceType: res.(*resource).resourceType,
+		id:           id,
+		object:       plannedObject.(map[string]interface{}),
+		meta:         d.meta,
+	}, nil
+}
+
 func (d *instanceDiff) Destroy() bool {
 	return d.destroy
 }


Test patch:
diff --git a/pkg/tfbridge/provider_test.go b/pkg/tfbridge/provider_test.go
index b65c8bb9..57286c45 100644
--- a/pkg/tfbridge/provider_test.go
+++ b/pkg/tfbridge/provider_test.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/pulumi/pulumi/sdk/v2/go/common/resource"
 	"github.com/pulumi/pulumi/sdk/v2/go/common/resource/plugin"
+	"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
 	pulumirpc "github.com/pulumi/pulumi/sdk/v2/proto/go"
 	"github.com/stretchr/testify/assert"
 
@@ -194,3 +195,80 @@ func TestBuildConfig(t *testing.T) {
 	})
 	assert.Equal(t, expected, configOut)
 }
+
+func TestProviderPreview(t *testing.T) {
+	provider := &Provider{
+		tf:     shimv1.NewProvider(testTFProvider),
+		config: shimv1.NewSchemaMap(testTFProvider.Schema),
+	}
+	provider.resources = map[tokens.Type]Resource{
+		"ExampleResource": {
+			TF:     shimv1.NewResource(testTFProvider.ResourcesMap["example_resource"]),
+			TFName: "example_resource",
+			Schema: &ResourceInfo{Tok: "ExampleResource"},
+		},
+	}
+
+	urn := resource.NewURN("stack", "project", "", "ExampleResource", "name")
+
+	// Step 1: create and check an input bag.
+	pulumiIns, err := plugin.MarshalProperties(resource.NewPropertyMapFromMap(map[string]interface{}{
+		"stringPropertyValue": "foo",
+		"setPropertyValue":    []interface{}{"foo"},
+	}), plugin.MarshalOptions{})
+	assert.NoError(t, err)
+	checkResp, err := provider.Check(context.Background(), &pulumirpc.CheckRequest{
+		Urn:  string(urn),
+		News: pulumiIns,
+	})
+	assert.NoError(t, err)
+
+	// Step 2a: preview the creation of a resource using the checked input bag.
+	createResp, err := provider.Create(context.Background(), &pulumirpc.CreateRequest{
+		Urn:        string(urn),
+		Properties: checkResp.GetInputs(),
+		Preview:    true,
+	})
+	assert.NoError(t, err)
+
+	outs, err := plugin.UnmarshalProperties(createResp.GetProperties(), plugin.MarshalOptions{})
+	assert.NoError(t, err)
+	assert.True(t, resource.NewPropertyMapFromMap(map[string]interface{}{
+		"id":                  "",
+		"stringPropertyValue": "foo",
+		"setPropertyValues":   []interface{}{"foo"},
+	}).DeepEquals(outs))
+
+	// Step 2b: actually create the resource.
+	createResp, err = provider.Create(context.Background(), &pulumirpc.CreateRequest{
+		Urn:        string(urn),
+		Properties: checkResp.GetInputs(),
+	})
+	assert.NoError(t, err)
+
+	// Step 3: preview an update to the resource we just created.
+	pulumiIns, err = plugin.MarshalProperties(resource.NewPropertyMapFromMap(map[string]interface{}{
+		"stringPropertyValue": "bar",
+		"setPropertyValue":    []interface{}{"foo"},
+	}), plugin.MarshalOptions{})
+	assert.NoError(t, err)
+	checkResp, err = provider.Check(context.Background(), &pulumirpc.CheckRequest{
+		Urn:  string(urn),
+		News: pulumiIns,
+		Olds: createResp.GetProperties(),
+	})
+	assert.NoError(t, err)
+
+	updateResp, err := provider.Update(context.Background(), &pulumirpc.UpdateRequest{
+		Id:      "MyID",
+		Urn:     string(urn),
+		Olds:    createResp.GetProperties(),
+		News:    checkResp.GetInputs(),
+		Preview: true,
+	})
+	assert.NoError(t, err)
+
+	outs, err = plugin.UnmarshalProperties(updateResp.GetProperties(), plugin.MarshalOptions{})
+	assert.NoError(t, err)
+	assert.Equal(t, resource.NewStringProperty("bar"), outs["stringPropertyValue"])
+}
diff --git a/pkg/tfshim/sdk-v1/instance_state_test.go b/pkg/tfshim/sdk-v1/instance_state_test.go
index a2793beb..70258f00 100644
--- a/pkg/tfshim/sdk-v1/instance_state_test.go
+++ b/pkg/tfshim/sdk-v1/instance_state_test.go
@@ -181,3 +181,127 @@ func TestEmptyListAttribute(t *testing.T) {
 		"list_property.#": "0",
 	})
 }
+
+func TestObjectFromInstanceDiff(t *testing.T) {
+	res := NewResource(&schema.Resource{
+		Schema: map[string]*schema.Schema{
+			"nil_property_value":    {Type: schema.TypeMap},
+			"bool_property_value":   {Type: schema.TypeBool},
+			"number_property_value": {Type: schema.TypeInt},
+			"float_property_value":  {Type: schema.TypeFloat},
+			"string_property_value": {Type: schema.TypeString},
+			"array_property_value": {
+				Type: schema.TypeList,
+				Elem: &schema.Schema{Type: schema.TypeString},
+			},
+			"object_property_value": {Type: schema.TypeMap},
+			"map_property_value":    {Type: schema.TypeMap},
+			"nested_resources": {
+				Type:     schema.TypeList,
+				MaxItems: 1,
+				// Embed a `*schema.Resource` to validate that type directed
+				// walk of the schema successfully walks inside Resources as well
+				// as Schemas.
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"configuration": {Type: schema.TypeMap},
+					},
+				},
+			},
+			"set_property_value": {
+				Type: schema.TypeSet,
+				Elem: &schema.Schema{Type: schema.TypeString},
+			},
+			"string_with_bad_interpolation": {Type: schema.TypeString},
+			"removed_property_value": {
+				Type:    schema.TypeString,
+				Removed: "Removed in the Terraform provider",
+			},
+		},
+	})
+
+	state, err := res.InstanceState("id", map[string]interface{}{
+		"nil_property_value":    nil,
+		"bool_property_value":   false,
+		"number_property_value": 42,
+		"float_property_value":  99.6767932,
+		"string_property_value": "ognirts",
+		"array_property_value":  []interface{}{"an array"},
+		"object_property_value": map[string]interface{}{
+			"property_a": "a",
+			"property_b": true,
+		},
+		"map_property_value": map[string]interface{}{
+			"propertyA": "a",
+			"propertyB": true,
+			"propertyC": map[string]interface{}{
+				"nestedPropertyA": true,
+			},
+		},
+		"nested_resources": []interface{}{
+			map[string]interface{}{
+				"configuration": map[string]interface{}{
+					"configurationValue": true,
+				},
+			},
+		},
+		"set_property_value":            []interface{}{"set member 1", "set member 2"},
+		"string_with_bad_interpolation": "some ${interpolated:value} with syntax errors",
+		"removed_property_value":        "a removed property",
+	}, nil)
+	assert.NoError(t, err)
+
+	s := state.(v1InstanceState)
+	s.diff = &terraform.InstanceDiff{
+		Attributes: map[string]*terraform.ResourceAttrDiff{
+			"number_property_value": {
+				Old:         "42",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"object_property_value.property_a": {
+				Old:         "a",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"map_property_value.%": {
+				Old:         "3",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"nested_resources.0.configuration.configurationValue": {
+				Old:         "true",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"set_property_value.1234": {
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+		},
+	}
+
+	obj, err := s.Object(res.Schema())
+	assert.NoError(t, err)
+
+	assert.Equal(t, map[string]interface{}{
+		"id":                    "",
+		"bool_property_value":   false,
+		"float_property_value":  99.6767932,
+		"string_property_value": "ognirts",
+		"array_property_value":  []interface{}{"an array"},
+		"object_property_value": map[string]interface{}{
+			"property_a": UnknownVariableValue,
+			"property_b": "true",
+		},
+		"nested_resources": []interface{}{
+			map[string]interface{}{
+				"configuration": map[string]interface{}{
+					"configurationValue": UnknownVariableValue,
+				},
+			},
+		},
+		"string_with_bad_interpolation": "some ${interpolated:value} with syntax errors",
+		"removed_property_value":        "a removed property",
+	}, obj)
+}
diff --git a/pkg/tfshim/sdk-v2/instance_state_test.go b/pkg/tfshim/sdk-v2/instance_state_test.go
index 72df6bdd..1badc3d0 100644
--- a/pkg/tfshim/sdk-v2/instance_state_test.go
+++ b/pkg/tfshim/sdk-v2/instance_state_test.go
@@ -175,3 +175,126 @@ func TestEmptyListAttribute(t *testing.T) {
 		"list_property.#": "0",
 	})
 }
+
+func TestObjectFromInstanceDiff(t *testing.T) {
+	res := NewResource(&schema.Resource{
+		Schema: map[string]*schema.Schema{
+			"nil_property_value":    {Type: schema.TypeMap},
+			"bool_property_value":   {Type: schema.TypeBool},
+			"number_property_value": {Type: schema.TypeInt},
+			"float_property_value":  {Type: schema.TypeFloat},
+			"string_property_value": {Type: schema.TypeString},
+			"array_property_value": {
+				Type: schema.TypeList,
+				Elem: &schema.Schema{Type: schema.TypeString},
+			},
+			"object_property_value": {Type: schema.TypeMap},
+			"map_property_value":    {Type: schema.TypeMap},
+			"nested_resources": {
+				Type:     schema.TypeList,
+				MaxItems: 1,
+				// Embed a `*schema.Resource` to validate that type directed
+				// walk of the schema successfully walks inside Resources as well
+				// as Schemas.
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"configuration": {Type: schema.TypeMap},
+					},
+				},
+			},
+			"set_property_value": {
+				Type: schema.TypeSet,
+				Elem: &schema.Schema{Type: schema.TypeString},
+			},
+			"string_with_bad_interpolation": {Type: schema.TypeString},
+			"removed_property_value": {
+				Type: schema.TypeString,
+			},
+		},
+	})
+
+	state, err := res.InstanceState("id", map[string]interface{}{
+		"nil_property_value":    nil,
+		"bool_property_value":   false,
+		"number_property_value": 42,
+		"float_property_value":  99.6767932,
+		"string_property_value": "ognirts",
+		"array_property_value":  []interface{}{"an array"},
+		"object_property_value": map[string]interface{}{
+			"property_a": "a",
+			"property_b": true,
+		},
+		"map_property_value": map[string]interface{}{
+			"propertyA": "a",
+			"propertyB": true,
+			"propertyC": map[string]interface{}{
+				"nestedPropertyA": true,
+			},
+		},
+		"nested_resources": []interface{}{
+			map[string]interface{}{
+				"configuration": map[string]interface{}{
+					"configurationValue": true,
+				},
+			},
+		},
+		"set_property_value":            []interface{}{"set member 1", "set member 2"},
+		"string_with_bad_interpolation": "some ${interpolated:value} with syntax errors",
+		"removed_property_value":        "a removed property",
+	}, nil)
+	assert.NoError(t, err)
+
+	s := state.(v2InstanceState)
+	s.diff = &terraform.InstanceDiff{
+		Attributes: map[string]*terraform.ResourceAttrDiff{
+			"number_property_value": {
+				Old:         "42",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"object_property_value.property_a": {
+				Old:         "a",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"map_property_value.%": {
+				Old:         "3",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"nested_resources.0.configuration.configurationValue": {
+				Old:         "true",
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+			"set_property_value.1234": {
+				New:         UnknownVariableValue,
+				NewComputed: true,
+			},
+		},
+	}
+
+	obj, err := s.Object(res.Schema())
+	assert.NoError(t, err)
+
+	assert.Equal(t, map[string]interface{}{
+		"id":                    "",
+		"bool_property_value":   false,
+		"float_property_value":  99.6767932,
+		"string_property_value": "ognirts",
+		"array_property_value":  []interface{}{"an array"},
+		"object_property_value": map[string]interface{}{
+			"property_a": UnknownVariableValue,
+			"property_b": "true",
+		},
+		"nested_resources": []interface{}{
+			map[string]interface{}{
+				"configuration": map[string]interface{}{
+					"configurationValue": UnknownVariableValue,
+				},
+			},
+		},
+		"string_with_bad_interpolation": "some ${interpolated:value} with syntax errors",
+		"removed_property_value":        "a removed property",
+	}, obj)
+}