Repository: pulumi/pulumi-terraform-bridge
Pull request #453 (2022-02-16 13:11:29)

Elide empty inputs from Read
Currently when we issue a refresh to terraform it sets properties to a zero value if they were reported unset by the underlying provider. This results in a lot of properties with “”, false, or 0 values which should have just been unset. 

This commit uses the schema reported back to strip out these empty properties. This should result in much better Read results from the tfbridge providers.


github-actions[bot]: Diff for [pulumi-random](https://github.com/pulumi-bot/pulumi-random/compare/f87c2c3..49114df) with merge commit ad0fa87a9a436fb7e8ccecd508c22dbf51cb6d01
github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/2f0a9ae..101bdb4) with merge commit ad0fa87a9a436fb7e8ccecd508c22dbf51cb6d01
github-actions[bot]: Diff for [pulumi-gcp](https://github.com/pulumi-bot/pulumi-gcp/compare/f3d5f0fa9..d7996acc9) with merge commit ad0fa87a9a436fb7e8ccecd508c22dbf51cb6d01
github-actions[bot]: Diff for [pulumi-azure](https://github.com/pulumi-bot/pulumi-azure/compare/59dff1731..cc5fdf154) with merge commit ad0fa87a9a436fb7e8ccecd508c22dbf51cb6d01
github-actions[bot]: Diff for [pulumi-aws](https://github.com/pulumi-bot/pulumi-aws/compare/99fe06278..17633fd9e) with merge commit ad0fa87a9a436fb7e8ccecd508c22dbf51cb6d01
github-actions[bot]: Diff for [pulumi-random](https://github.com/pulumi-bot/pulumi-random/compare/3a827de..667caaa) with merge commit b84a5da2d244b979b8b9eff8212f80a9a43e3da8
github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/b89e568..46095ff) with merge commit b84a5da2d244b979b8b9eff8212f80a9a43e3da8
github-actions[bot]: Diff for [pulumi-aws](https://github.com/pulumi-bot/pulumi-aws/compare/918a9b209..945183996) with merge commit b84a5da2d244b979b8b9eff8212f80a9a43e3da8
jkodroff: Assigning to @stack72 for review once he returns since I'm not familiar enough with tfbridge to give a meaningful review.
mikhailshilkov: We can't wait for @stack72 if we want to meet the Import delivery on March 9... Hoping @pgavlin can help here.
stack72: I'm happy with the code here - I'd really like to see if there's a short video of a smoke test in the following scenario:

## no provider CRUD functionality changes 
* create new s3 bucket with the latest released version of Pulumi-aws
* build a new version of the aws provider with this latest bridge
* "upgrade" to this new functionality and ensure that the behaviour still works as expected 

Frassle: @stack72 I can run that test. Will get back with an update in a bit.

Patch:
diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go
index 7b553c1c..8861c263 100644
--- a/pkg/tfbridge/schema.go
+++ b/pkg/tfbridge/schema.go
@@ -25,7 +25,6 @@ import (
 
 	"github.com/golang/glog"
 	pbstruct "github.com/golang/protobuf/ptypes/struct"
-	"github.com/mitchellh/copystructure"
 	"github.com/pkg/errors"
 	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
 	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
@@ -1100,43 +1099,95 @@ func extractInputs(oldInput, newState resource.PropertyValue, tfs shim.Schema, p
 	}
 }
 
-func addDefaultAnnotations(newInput resource.PropertyValue) {
+func getDefaultValue(tfs shim.Schema, ps *SchemaInfo) interface{} {
+	if dv, _ := tfs.DefaultValue(); dv != nil {
+		return dv
+	}
+	if ps != nil {
+		return ps.Default
+	}
+	return nil
+}
+
+func isDefaultOrZeroValue(tfs shim.Schema, ps *SchemaInfo, v resource.PropertyValue) bool {
+	if dv := getDefaultValue(tfs, ps); dv != nil {
+		return dv == v.V
+	}
+
 	switch {
-	case newInput.IsArray():
-		newArray := newInput.ArrayValue()
-		for i := range newArray {
-			addDefaultAnnotations(newArray[i])
-		}
-	case newInput.IsObject():
-		newMap := newInput.ObjectValue()
-		for _, newValue := range newMap {
-			addDefaultAnnotations(newValue)
+	case v.IsNull():
+		return true
+	case v.IsBool():
+		return v.BoolValue() == false
+	case v.IsNumber():
+		return v.NumberValue() == 0
+	case v.IsString():
+		return v.StringValue() == ""
+	case v.IsArray():
+		return len(v.ArrayValue()) == 0
+	case v.IsObject():
+		obj := v.ObjectValue()
+		switch len(obj) {
+		case 0:
+			return true
+		case 1:
+			_, ok := obj[defaultsKey]
+			return ok
+		default:
+			return false
 		}
-		newMap[defaultsKey] = resource.NewArrayProperty([]resource.PropertyValue{})
+	default:
+		return false
 	}
 }
 
-func extractSchemaInputs(state resource.PropertyValue, tfs shim.SchemaMap,
-	ps map[string]*SchemaInfo) (resource.PropertyValue, error) {
-	inputs := make(resource.PropertyMap)
-	for name, value := range state.ObjectValue() {
-		// If this property is not an input, ignore it.
-		_, sch, _ := getInfoFromPulumiName(name, tfs, ps, false)
-		if sch == nil || (!sch.Optional() && !sch.Required()) {
-			continue
+func extractSchemaInputs(state resource.PropertyValue, tfs shim.Schema, ps *SchemaInfo,
+	rawNames bool) resource.PropertyValue {
+
+	switch {
+	case state.IsArray():
+		etfs, eps := elemSchemas(tfs, ps)
+
+		a := state.ArrayValue()
+		v := make([]resource.PropertyValue, len(a))
+		for i := range a {
+			v[i] = extractSchemaInputs(a[i], etfs, eps, rawNames || useRawNames(tfs))
+		}
+		return resource.NewArrayProperty(v)
+	case state.IsObject():
+		var tfflds shim.SchemaMap
+		if tfs != nil {
+			if res, isres := tfs.Elem().(shim.Resource); isres {
+				tfflds = res.Schema()
+			}
+		}
+		var psflds map[string]*SchemaInfo
+		if ps != nil {
+			psflds = ps.Fields
 		}
+		isMap := tfflds == nil
+
+		obj := state.ObjectValue()
+		v := make(map[resource.PropertyKey]resource.PropertyValue, len(obj))
+		for k, e := range obj {
+			_, etfs, eps := getInfoFromPulumiName(k, tfflds, psflds, rawNames || useRawNames(tfs))
+			if isInput := isMap || etfs != nil && (etfs.Optional() || etfs.Required()); !isInput {
+				glog.V(9).Infof("skipping '%v' (not an input)", k)
+				continue
+			}
 
-		// Otherwise, copy it to the result.
-		copy, err := copystructure.Copy(value)
-		if err != nil {
-			return resource.PropertyValue{}, err
+			ev := extractSchemaInputs(e, etfs, eps, rawNames || useRawNames(tfs))
+			if mustSet := isMap || etfs != nil && (etfs.Required() || !isDefaultOrZeroValue(etfs, eps, ev)); !mustSet {
+				glog.V(9).Infof("skipping '%v' (not required + default or zero value)", k)
+				continue
+			}
+			v[k] = ev
 		}
-		inputs[name] = copy.(resource.PropertyValue)
+		v[defaultsKey] = resource.NewArrayProperty([]resource.PropertyValue{})
+		return resource.NewObjectProperty(v)
+	default:
+		return state
 	}
-
-	inputsValue := resource.NewObjectProperty(inputs)
-	addDefaultAnnotations(inputsValue)
-	return inputsValue, nil
 }
 
 func extractInputsFromOutputs(oldInputs, outs resource.PropertyMap,
@@ -1156,11 +1207,7 @@ func extractInputsFromOutputs(oldInputs, outs resource.PropertyMap,
 			resource.NewObjectProperty(outs), sch, pss, false)
 	} else {
 		// Otherwise, take a schema-directed approach that fills out all input-only properties.
-		v, err := extractSchemaInputs(resource.NewObjectProperty(outs), tfs, ps)
-		if err != nil {
-			return nil, err
-		}
-		inputs = v
+		inputs = extractSchemaInputs(resource.NewObjectProperty(outs), sch, pss, false)
 	}
 	return inputs.ObjectValue(), nil
 }
diff --git a/pkg/tfgen/docs.go b/pkg/tfgen/docs.go
index ee583337..30eb1532 100644
--- a/pkg/tfgen/docs.go
+++ b/pkg/tfgen/docs.go
@@ -175,9 +175,8 @@ func getRepoPath(gitHost string, org string, provider string, version string) (s
 	return target.Dir, nil
 }
 
-func getMarkdownDetails(g *Generator, org string, provider string, resourcePrefix string, kind DocKind,
-	rawname string, info tfbridge.ResourceOrDataSourceInfo, providerModuleVersion string,
-	githost string) ([]byte, string, bool) {
+func getMarkdownDetails(org string, provider string, resourcePrefix string, kind DocKind, rawname string,
+	info tfbridge.ResourceOrDataSourceInfo, providerModuleVersion string, githost string) ([]byte, string, bool) {
 
 	var docinfo *tfbridge.DocInfo
 	if info != nil {
@@ -227,8 +226,7 @@ func getDocsForProvider(g *Generator, org string, provider string, resourcePrefi
 		return entityDocs{}, nil
 	}
 
-	markdownBytes, markdownFileName, found := getMarkdownDetails(g, org, provider, resourcePrefix, kind, rawname, info,
-		providerModuleVersion, githost)
+	markdownBytes, markdownFileName, found := getMarkdownDetails(org, provider, resourcePrefix, kind, rawname, info, providerModuleVersion, githost)
 	if !found {
 		g.warn("Could not find docs for resource %v; consider overriding doc source location", rawname)
 		return entityDocs{}, nil
@@ -245,23 +243,17 @@ func getDocsForProvider(g *Generator, org string, provider string, resourcePrefi
 	}
 	if docinfo != nil {
 		// Merge Attributes from source into target
-		if err := mergeDocs(g, info, org, provider, resourcePrefix, kind, doc,
-			docinfo.IncludeAttributesFrom, true, true,
-			providerModuleVersion, githost); err != nil {
+		if err := mergeDocs(g, org, provider, resourcePrefix, kind, doc, docinfo.IncludeAttributesFrom, true, true, providerModuleVersion, githost); err != nil {
 			return doc, err
 		}
 
 		// Merge Arguments from source into Attributes of target
-		if err := mergeDocs(g, info, org, provider, resourcePrefix, kind, doc,
-			docinfo.IncludeAttributesFromArguments, true, false,
-			providerModuleVersion, githost); err != nil {
+		if err := mergeDocs(g, org, provider, resourcePrefix, kind, doc, docinfo.IncludeAttributesFromArguments, true, false, providerModuleVersion, githost); err != nil {
 			return doc, err
 		}
 
 		// Merge Arguments from source into target
-		if err := mergeDocs(g, info, org, provider, provider, kind, doc,
-			docinfo.IncludeArgumentsFrom, false, false,
-			providerModuleVersion, githost); err != nil {
+		if err := mergeDocs(g, org, provider, provider, kind, doc, docinfo.IncludeArgumentsFrom, false, false, providerModuleVersion, githost); err != nil {
 			return doc, err
 		}
 	}
@@ -308,10 +300,9 @@ func readMarkdown(repo string, kind DocKind, possibleLocations []string) ([]byte
 }
 
 // mergeDocs adds the docs specified by extractDoc from sourceFrom into the targetDocs
-func mergeDocs(g *Generator, info tfbridge.ResourceOrDataSourceInfo, org string, provider string,
-	resourcePrefix string,
-	kind DocKind, docs entityDocs, sourceFrom string,
-	useTargetAttributes bool, useSourceAttributes bool, providerModuleVersion string, githost string) error {
+func mergeDocs(g *Generator, org string, provider string, resourcePrefix string, kind DocKind, docs entityDocs,
+	sourceFrom string, useTargetAttributes bool, useSourceAttributes bool, providerModuleVersion string,
+	githost string) error {
 
 	if sourceFrom != "" {
 		sourceDocs, err := getDocsForProvider(g, org, provider, resourcePrefix, kind,
@@ -377,7 +368,7 @@ var (
 	attributionFormatString = "This Pulumi package is based on the [`%[1]s` Terraform Provider](https://%[3]s/%[2]s/terraform-provider-%[1]s)."
 )
 
-// groupLines groups a collection of strings, a, by a given separator, sep.
+// groupLines groups a collection of strings, lines, by a given separator, sep.
 func groupLines(lines []string, sep string) [][]string {
 	var buffer []string
 	var sections [][]string
@@ -453,7 +444,7 @@ func (p *tfMarkdownParser) parse() (entityDocs, error) {
 	sections := splitGroupLines(markdown, "## ")
 
 	// Reparent examples that are peers of the "Example Usage" section (if any) and fixup some example titles.
-	sections = p.reformatExamples(sections)
+	sections = reformatExamples(sections)
 
 	for _, section := range sections {
 		if err := p.parseSection(section); err != nil {
@@ -473,7 +464,7 @@ func (p *tfMarkdownParser) parse() (entityDocs, error) {
 }
 
 // fixExampleTitles transforms H4 sections that contain code snippets into H3 sections.
-func (p *tfMarkdownParser) fixExampleTitles(lines []string) {
+func fixExampleTitles(lines []string) {
 	inSection, sectionIndex := false, 0
 	for i, line := range lines {
 		if inSection && strings.HasPrefix(line, "```") {
@@ -489,7 +480,7 @@ var exampleHeaderRegexp = regexp.MustCompile(`(?i)^(## Example Usage\s*)(?:(?:(?
 
 // reformatExamples reparents examples that are peers of the "Example Usage" section (if any) and fixup some example
 // titles.
-func (p *tfMarkdownParser) reformatExamples(sections [][]string) [][]string {
+func reformatExamples(sections [][]string) [][]string {
 	canonicalExampleUsageSectionIndex := -1
 	var exampleUsageSection []string
 	var exampleSectionIndices []int
@@ -553,7 +544,7 @@ func (p *tfMarkdownParser) reformatExamples(sections [][]string) [][]string {
 	exampleUsageSection[0] = "## Example Usage"
 
 	// Fixup example titles and replace the contents of the canonical example usage section with the output.
-	p.fixExampleTitles(exampleUsageSection)
+	fixExampleTitles(exampleUsageSection)
 	sections[canonicalExampleUsageSectionIndex] = exampleUsageSection
 
 	// If there is only one example section, we're done. Otherwise, we need to remove all non-canonical example usage
@@ -971,7 +962,7 @@ func (p *tfMarkdownParser) reformatSubsection(lines []string) ([]string, bool, b
 	return result, hasExamples, isEmpty
 }
 
-// parseExamples converts any code snippets in a subsection to Pulumi-compatible code. This conversion is done on a
+// convertExamples converts any code snippets in a subsection to Pulumi-compatible code. This conversion is done on a
 // per-subsection basis; subsections with failing examples will be elided upon the caller's request.
 func (g *Generator) convertExamples(docs, name string, stripSubsectionsWithErrors bool) string {
 	if docs == "" {


Test patch:
diff --git a/internal/testprovider/schema.go b/internal/testprovider/schema.go
index 06263e2a..95892eeb 100644
--- a/internal/testprovider/schema.go
+++ b/internal/testprovider/schema.go
@@ -65,6 +65,7 @@ func ProviderV1() *schemav1.Provider {
 						Elem: &schemav1.Resource{
 							Schema: map[string]*schemav1.Schema{
 								"kind":          {Type: schemav1.TypeString, Optional: true},
+								"opt_bool":      {Type: schemav1.TypeBool, Optional: true},
 								"configuration": {Type: schemav1.TypeMap, Required: true},
 							},
 						},
@@ -106,25 +107,30 @@ func ProviderV1() *schemav1.Provider {
 					return nil
 				},
 				Read: func(data *schemav1.ResourceData, p interface{}) error {
-					MustSetIfUnset(data, "bool_property_value", false)
-					MustSetIfUnset(data, "number_property_value", 42)
-					MustSetIfUnset(data, "float_property_value", 99.6767932)
-					MustSetIfUnset(data, "string_property_value", "ognirts")
-					MustSetIfUnset(data, "array_property_value", []interface{}{"an array"})
-					MustSetIfUnset(data, "object_property_value", map[string]interface{}{
-						"property_a": "a",
-						"property_b": "true",
-						"property.c": "some.value",
-					})
-					MustSetIfUnset(data, "nested_resources", []interface{}{
-						map[string]interface{}{
-							"configuration": map[string]interface{}{
-								"configurationValue": "true",
+					if data.Id() == "empty-resource-id" {
+						MustSetIfUnset(data, "array_property_value", []interface{}{"an array"})
+						MustSetIfUnset(data, "nested_resources", nil)
+					} else {
+						MustSetIfUnset(data, "bool_property_value", false)
+						MustSetIfUnset(data, "number_property_value", 42)
+						MustSetIfUnset(data, "float_property_value", 99.6767932)
+						MustSetIfUnset(data, "string_property_value", "ognirts")
+						MustSetIfUnset(data, "array_property_value", []interface{}{"an array"})
+						MustSetIfUnset(data, "object_property_value", map[string]interface{}{
+							"property_a": "a",
+							"property_b": "true",
+							"property.c": "some.value",
+						})
+						MustSetIfUnset(data, "nested_resources", []interface{}{
+							map[string]interface{}{
+								"configuration": map[string]interface{}{
+									"configurationValue": "true",
+								},
 							},
-						},
-					})
-					MustSetIfUnset(data, "set_property_value", []interface{}{"set member 1", "set member 2"})
-					MustSetIfUnset(data, "string_with_bad_interpolation", "some ${interpolated:value} with syntax errors")
+						})
+						MustSetIfUnset(data, "set_property_value", []interface{}{"set member 1", "set member 2"})
+						MustSetIfUnset(data, "string_with_bad_interpolation", "some ${interpolated:value} with syntax errors")
+					}
 					return nil
 				},
 				Update: func(data *schemav1.ResourceData, p interface{}) error {
@@ -380,6 +386,7 @@ func ProviderV2() *schemav2.Provider {
 						Elem: &schemav2.Resource{
 							Schema: map[string]*schemav2.Schema{
 								"kind":          {Type: schemav2.TypeString, Optional: true},
+								"opt_bool":      {Type: schemav2.TypeBool, Optional: true},
 								"configuration": {Type: schemav2.TypeMap, Required: true},
 							},
 						},
@@ -421,25 +428,30 @@ func ProviderV2() *schemav2.Provider {
 					return nil
 				},
 				Read: func(data *schemav2.ResourceData, p interface{}) error {
-					MustSetIfUnset(data, "bool_property_value", false)
-					MustSetIfUnset(data, "number_property_value", 42)
-					MustSetIfUnset(data, "float_property_value", 99.6767932)
-					MustSetIfUnset(data, "string_property_value", "ognirts")
-					MustSetIfUnset(data, "array_property_value", []interface{}{"an array"})
-					MustSetIfUnset(data, "object_property_value", map[string]interface{}{
-						"property_a": "a",
-						"property_b": "true",
-						"property.c": "some.value",
-					})
-					MustSetIfUnset(data, "nested_resources", []interface{}{
-						map[string]interface{}{
-							"configuration": map[string]interface{}{
-								"configurationValue": "true",
+					if data.Id() == "empty-resource-id" {
+						MustSetIfUnset(data, "array_property_value", []interface{}{"an array"})
+						MustSetIfUnset(data, "nested_resources", nil)
+					} else {
+						MustSetIfUnset(data, "bool_property_value", false)
+						MustSetIfUnset(data, "number_property_value", 42)
+						MustSetIfUnset(data, "float_property_value", 99.6767932)
+						MustSetIfUnset(data, "string_property_value", "ognirts")
+						MustSetIfUnset(data, "array_property_value", []interface{}{"an array"})
+						MustSetIfUnset(data, "object_property_value", map[string]interface{}{
+							"property_a": "a",
+							"property_b": "true",
+							"property.c": "some.value",
+						})
+						MustSetIfUnset(data, "nested_resources", []interface{}{
+							map[string]interface{}{
+								"configuration": map[string]interface{}{
+									"configurationValue": "true",
+								},
 							},
-						},
-					})
-					MustSetIfUnset(data, "set_property_value", []interface{}{"set member 1", "set member 2"})
-					MustSetIfUnset(data, "string_with_bad_interpolation", "some ${interpolated:value} with syntax errors")
+						})
+						MustSetIfUnset(data, "set_property_value", []interface{}{"set member 1", "set member 2"})
+						MustSetIfUnset(data, "string_with_bad_interpolation", "some ${interpolated:value} with syntax errors")
+					}
 					return nil
 				},
 				Update: func(data *schemav2.ResourceData, p interface{}) error {
diff --git a/pkg/tfbridge/provider_test.go b/pkg/tfbridge/provider_test.go
index 44182744..81c87cac 100644
--- a/pkg/tfbridge/provider_test.go
+++ b/pkg/tfbridge/provider_test.go
@@ -353,6 +353,7 @@ func testProviderPreview(t *testing.T, provider *Provider) {
 			"configuration": resource.NewObjectProperty(resource.PropertyMap{
 				"name": resource.NewStringProperty("foo"),
 			}),
+			"optBool": resource.NewBoolProperty(false),
 		}),
 	}.DeepEquals(outs))
 
@@ -415,6 +416,7 @@ func testProviderPreview(t *testing.T, provider *Provider) {
 		"configuration": resource.NewObjectProperty(resource.PropertyMap{
 			"name": resource.NewStringProperty("foo"),
 		}),
+		"optBool": resource.NewBoolProperty(false),
 	}).DeepEquals(outs["nestedResources"]))
 }
 
@@ -554,3 +556,103 @@ func TestProviderPreConfigureCallbackV2(t *testing.T) {
 	}
 	testProviderPreConfigureCallback(t, provider)
 }
+
+func testProviderRead(t *testing.T, provider *Provider, typeName tokens.Type) {
+	urn := resource.NewURN("stack", "project", "", typeName, "name")
+	readResp, err := provider.Read(context.Background(), &pulumirpc.ReadRequest{
+		Id:         string("resource-id"),
+		Urn:        string(urn),
+		Properties: nil,
+	})
+	assert.NoError(t, err)
+
+	assert.NotNil(t, readResp.GetInputs())
+	assert.NotNil(t, readResp.GetProperties())
+
+	ins, err := plugin.UnmarshalProperties(readResp.GetInputs(), plugin.MarshalOptions{KeepUnknowns: true})
+	assert.NoError(t, err)
+	// Check all the expected inputs were read
+	assert.NotContains(t, ins, "boolPropertyValue") // This was "false" from Read, but it's default is false
+	assert.Equal(t, resource.NewNumberProperty(42), ins["numberPropertyValue"])
+	assert.Equal(t, resource.NewNumberProperty(99.6767932), ins["floatPropertyValue"])
+	assert.Equal(t, resource.NewStringProperty("ognirts"), ins["stringPropertyValue"])
+	assert.Equal(t, resource.NewArrayProperty(
+		[]resource.PropertyValue{resource.NewStringProperty("an array")}), ins["arrayPropertyValues"])
+	assert.Equal(t, resource.NewObjectProperty(resource.PropertyMap{
+		"__defaults": resource.NewArrayProperty([]resource.PropertyValue{}),
+		"property_a": resource.NewStringProperty("a"),
+		"property_b": resource.NewStringProperty("true"),
+		"property.c": resource.NewStringProperty("some.value"),
+	}), ins["objectPropertyValue"])
+	assert.Equal(t, resource.NewObjectProperty(resource.PropertyMap{
+		"__defaults": resource.NewArrayProperty([]resource.PropertyValue{}),
+		"configuration": resource.NewObjectProperty(resource.PropertyMap{
+			"__defaults":         resource.NewArrayProperty([]resource.PropertyValue{}),
+			"configurationValue": resource.NewStringProperty("true"),
+		}),
+	}), ins["nestedResources"])
+	assert.Equal(t, resource.NewArrayProperty(
+		[]resource.PropertyValue{
+			resource.NewStringProperty("set member 2"),
+			resource.NewStringProperty("set member 1"),
+		}), ins["setPropertyValues"])
+	assert.Equal(t, resource.NewStringProperty("some ${interpolated:value} with syntax errors"), ins["stringWithBadInterpolation"])
+
+	// Read again with the ID that results in all the optinal fields not being set
+	readResp, err = provider.Read(context.Background(), &pulumirpc.ReadRequest{
+		Id:         string("empty-resource-id"),
+		Urn:        string(urn),
+		Properties: nil,
+	})
+	assert.NoError(t, err)
+
+	assert.NotNil(t, readResp.GetInputs())
+	assert.NotNil(t, readResp.GetProperties())
+
+	ins, err = plugin.UnmarshalProperties(readResp.GetInputs(), plugin.MarshalOptions{KeepUnknowns: true})
+	assert.NoError(t, err)
+	// Check all the expected inputs were read
+	assert.NotContains(t, ins, "boolPropertyValue")
+	assert.NotContains(t, ins, "numberPropertyValue")
+	assert.NotContains(t, ins, "floatPropertyValue")
+	assert.NotContains(t, ins, "stringPropertyValue")
+	assert.Equal(t, resource.NewArrayProperty(
+		[]resource.PropertyValue{resource.NewStringProperty("an array")}), ins["arrayPropertyValues"])
+	assert.NotContains(t, ins, "objectPropertyValue")
+	assert.NotContains(t, ins, "nestedResources")
+	assert.NotContains(t, ins, "setPropertyValues")
+	assert.NotContains(t, ins, "stringWithBadInterpolation")
+}
+
+func TestProviderReadV1(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"},
+		},
+	}
+
+	testProviderRead(t, provider, "ExampleResource")
+}
+
+func TestProviderReadV2(t *testing.T) {
+	provider := &Provider{
+		tf:     shimv2.NewProvider(testTFProviderV2),
+		config: shimv2.NewSchemaMap(testTFProviderV2.Schema),
+	}
+	provider.resources = map[tokens.Type]Resource{
+		"ExampleResource": {
+			TF:     shimv2.NewResource(testTFProviderV2.ResourcesMap["example_resource"]),
+			TFName: "example_resource",
+			Schema: &ResourceInfo{Tok: "ExampleResource"},
+		},
+	}
+
+	testProviderRead(t, provider, "ExampleResource")
+}
diff --git a/pkg/tfbridge/schema_test.go b/pkg/tfbridge/schema_test.go
index 396ee23d..be18b81d 100644
--- a/pkg/tfbridge/schema_test.go
+++ b/pkg/tfbridge/schema_test.go
@@ -1391,6 +1391,11 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 					return strings.ToLower(v.(string))
 				},
 			},
+
+			// input_i, inout_j, and inout_k test import scenarios where attributes are set to "".
+			"input_i": {Type: schemav1.TypeString, Required: true},
+			"inout_j": {Type: schemav1.TypeString, Optional: true, Computed: true},
+			"inout_k": {Type: schemav1.TypeString, Optional: true, Computed: true, Default: "inout_k_default"},
 		},
 		func(d *schemav1.ResourceData, meta interface{}) ([]*schemav1.ResourceData, error) {
 			return []*schemav1.ResourceData{d}, nil
@@ -1413,6 +1418,9 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		}
 		set(d, "inout_d", "inout_d_read")
 		set(d, "output_g", "output_g_read")
+		set(d, "input_i", "")
+		set(d, "inout_j", "")
+		set(d, "inout_k", "")
 		return nil
 	}
 	tfres.Create = func(d *schemav1.ResourceData, meta interface{}) error {
@@ -1470,6 +1478,9 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		"inoutC":  "inout_c_read",
 		"inoutD":  "inout_d_read",
 		"outputG": "output_g_read",
+		"inputI":  "",
+		"inoutJ":  "",
+		"inoutK":  "",
 	}), outs)
 
 	ins, err := plugin.UnmarshalProperties(resp.GetInputs(), plugin.MarshalOptions{})
@@ -1479,6 +1490,8 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		"inputA":    "input_a_read",
 		"inoutC":    "inout_c_read",
 		"inoutD":    "inout_d_read",
+		"inputI":    "",
+		"inoutK":    "",
 	})
 	assert.Equal(t, expected, ins)
 
@@ -1505,7 +1518,7 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 	assert.NoError(t, err)
 	sortDefaultsList(checkedIns)
 	assert.Equal(t, resource.NewPropertyMapFromMap(map[string]interface{}{
-		defaultsKey: []interface{}{"inoutD", "inputF", "inputH"},
+		defaultsKey: []interface{}{"inoutD", "inoutK", "inputF", "inputH"},
 		"inputA":    "input_a_create",
 		"inoutD":    "inout_d_default",
 		"inputE": map[string]interface{}{
@@ -1514,6 +1527,7 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		},
 		"inputF": "input_f_default",
 		"inputH": "Input_H_Default",
+		"inoutK": "inout_k_default",
 	}), checkedIns)
 
 	// Step 2: create a resource using the checked input bag. The inputs should be smuggled along with the state.
@@ -1536,6 +1550,7 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		"inputF":  "input_f_default",
 		"outputG": "output_g_create",
 		"inputH":  "input_h_default",
+		"inoutK":  "inout_k_default",
 	}), outs)
 
 	// Step 3: read the resource we just created. The read should make the following changes to the inputs:
@@ -1565,6 +1580,9 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		"inputF":  "input_f_default",
 		"outputG": "output_g_read",
 		"inputH":  "input_h_default",
+		"inputI":  "",
+		"inoutJ":  "",
+		"inoutK":  "",
 	}), outs)
 
 	ins, err = plugin.UnmarshalProperties(resp.GetInputs(), plugin.MarshalOptions{})
@@ -1579,6 +1597,7 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		},
 		"inputF": "input_f_default",
 		"inputH": "Input_H_Default",
+		"inoutK": "",
 	}), ins)
 
 	// Step 3a. delete the default annotations from the checked inputs and re-run the read. No default annotations
@@ -1609,6 +1628,9 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		"inputF":  "input_f_default",
 		"outputG": "output_g_read",
 		"inputH":  "input_h_default",
+		"inputI":  "",
+		"inoutJ":  "",
+		"inoutK":  "",
 	}), outs)
 
 	ins, err = plugin.UnmarshalProperties(resp.GetInputs(), plugin.MarshalOptions{})
@@ -1621,6 +1643,7 @@ func TestExtractInputsFromOutputs(t *testing.T) {
 		},
 		"inputF": "input_f_default",
 		"inputH": "Input_H_Default",
+		"inoutK": "",
 	}), ins)
 
 }
diff --git a/pkg/tfgen/docs_test.go b/pkg/tfgen/docs_test.go
index 7b158b3f..9ea4fee7 100644
--- a/pkg/tfgen/docs_test.go
+++ b/pkg/tfgen/docs_test.go
@@ -275,8 +275,6 @@ func TestReplaceFooterLinks(t *testing.T) {
 }
 
 func TestFixExamplesHeaders(t *testing.T) {
-	p := &tfMarkdownParser{}
-
 	codeFence := "```"
 	t.Run("WithCodeFences", func(t *testing.T) {
 		markdown := `
@@ -296,7 +294,7 @@ Provides a DigitalOcean CDN Endpoint resource for use with Spaces.
 		var processedMarkdown string
 		groups := splitGroupLines(markdown, "## ")
 		for _, lines := range groups {
-			p.fixExampleTitles(lines)
+			fixExampleTitles(lines)
 			for _, line := range lines {
 				processedMarkdown += line
 			}
@@ -323,7 +321,7 @@ Misleading example title without any actual code fences. We should not modify th
 		var processedMarkdown string
 		groups := splitGroupLines(markdown, "## ")
 		for _, lines := range groups {
-			p.fixExampleTitles(lines)
+			fixExampleTitles(lines)
 			for _, line := range lines {
 				processedMarkdown += line
 			}
@@ -356,3 +354,97 @@ Basic usage:`
 `
 	assert.Equal(t, "", extractExamples(multipleExampleUsages))
 }
+
+func TestReformatExamples(t *testing.T) {
+	runTest := func(input string, expected [][]string) {
+		inputSections := splitGroupLines(input, "## ")
+		output := reformatExamples(inputSections)
+
+		assert.ElementsMatch(t, expected, output)
+	}
+
+	// This is a simple use case. We expect no changes to the original doc:
+	simpleDoc := `description
+
+## Example Usage
+
+example usage content`
+
+	simpleDocExpected := [][]string{
+		{
+			"description",
+			"",
+		},
+		{
+			"## Example Usage",
+			"",
+			"example usage content",
+		},
+	}
+
+	runTest(simpleDoc, simpleDocExpected)
+
+	// This use case demonstrates 2 examples at the same H2 level: a canonical Example Usage and another example
+	// for a specific use case. We expect these to be transformed into a canonical H2 "Example Usage" with an H3 for
+	// the specific use case.
+	// This scenario is common in the pulumi-gcp provider:
+	gcpDoc := `description
+
+## Example Usage
+
+example usage content
+
+## Example Usage - Specific Case
+
+specific case content`
+
+	gcpDocExpected := [][]string{
+		{
+			"description",
+			"",
+		},
+		{
+			"## Example Usage",
+			"",
+			"example usage content",
+			"",
+			"### Specific Case",
+			"",
+			"specific case content",
+		},
+	}
+
+	runTest(gcpDoc, gcpDocExpected)
+
+	// This use case demonstrates 2 no canonical Example Usage/basic case and 2 specific use cases. We expect the
+	// function to add a canonical Example Usage section with the 2 use cases as H3's beneath the canonical section.
+	// This scenario is common in the pulumi-gcp provider:
+	gcpDoc2 := `description
+
+## Example Usage - 1
+
+content 1
+
+## Example Usage - 2
+
+content 2`
+
+	gcpDoc2Expected := [][]string{
+		{
+			"description",
+			"",
+		},
+		{
+			"## Example Usage",
+			"### 1",
+			"",
+			"content 1",
+			"",
+			"### 2",
+			"",
+			"content 2",
+		},
+	}
+
+	runTest(gcpDoc2, gcpDoc2Expected)
+}
diff --git a/pkg/tfshim/tfplugin5/provider_test.go b/pkg/tfshim/tfplugin5/provider_test.go
index 408ec293..3ad66dc2 100644
--- a/pkg/tfshim/tfplugin5/provider_test.go
+++ b/pkg/tfshim/tfplugin5/provider_test.go
@@ -231,6 +231,7 @@ func TestProviderResourcesMap(t *testing.T) {
 				"array_property_value":  cty.List(cty.String),
 				"object_property_value": cty.Map(cty.String),
 				"nested_resources": cty.List(cty.Object(map[string]cty.Type{
+					"opt_bool":      cty.Bool,
 					"kind":          cty.String,
 					"configuration": cty.Map(cty.String),
 				})),
@@ -311,16 +312,23 @@ func TestProviderResourcesMap(t *testing.T) {
 				},
 				"nested_resources": &attributeSchema{
 					ctyType: cty.List(cty.Object(map[string]cty.Type{
+						"opt_bool":      cty.Bool,
 						"kind":          cty.String,
 						"configuration": cty.Map(cty.String),
 					})),
 					valueType: shim.TypeList,
 					elem: &resource{
 						ctyType: cty.Object(map[string]cty.Type{
+							"opt_bool":      cty.Bool,
 							"kind":          cty.String,
 							"configuration": cty.Map(cty.String),
 						}),
 						schema: schema.SchemaMap{
+							"opt_bool": &attributeSchema{
+								ctyType:   cty.Bool,
+								valueType: shim.TypeBool,
+								optional:  true,
+							},
 							"kind": &attributeSchema{
 								ctyType:   cty.String,
 								valueType: shim.TypeString,
@@ -923,6 +931,7 @@ func TestDiff(t *testing.T) {
 				"array_property_value":  cty.ListValEmpty(cty.String),
 				"object_property_value": cty.NullVal(cty.Map(cty.String)),
 				"nested_resources": cty.ListValEmpty(cty.Object(map[string]cty.Type{
+					"opt_bool":      cty.Bool,
 					"kind":          cty.String,
 					"configuration": cty.Map(cty.String),
 				})),
@@ -1032,7 +1041,8 @@ func TestApply(t *testing.T) {
 				"array_property_value": []interface{}{"foo"},
 				"nested_resources": []interface{}{
 					map[string]interface{}{
-						"kind": cty.StringVal(""),
+						"opt_bool": cty.BoolVal(false),
+						"kind":     cty.StringVal(""),
 						"configuration": map[string]interface{}{
 							"baz": "qux",
 						},
@@ -1060,7 +1070,8 @@ func TestApply(t *testing.T) {
 					"property.c": cty.StringVal("some.value"),
 				}),
 				"nested_resources": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
-					"kind": cty.StringVal(""),
+					"opt_bool": cty.BoolVal(false),
+					"kind":     cty.StringVal(""),
 					"configuration": cty.MapVal(map[string]cty.Value{
 						"configurationValue": cty.StringVal("true"),
 					}),
@@ -1112,7 +1123,8 @@ func TestApply(t *testing.T) {
 						"property.c": cty.StringVal("some.value"),
 					}),
 					"nested_resources": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
-						"kind": cty.StringVal(""),
+						"opt_bool": cty.BoolVal(false),
+						"kind":     cty.StringVal(""),
 						"configuration": cty.MapVal(map[string]cty.Value{
 							"configurationValue": cty.StringVal("true"),
 						}),
@@ -1198,7 +1210,8 @@ func TestRefresh(t *testing.T) {
 			"property.c": cty.StringVal("some.value"),
 		}),
 		"nested_resources": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
-			"kind": cty.StringVal(""),
+			"opt_bool": cty.BoolVal(false),
+			"kind":     cty.StringVal(""),
 			"configuration": cty.MapVal(map[string]cty.Value{
 				"configurationValue": cty.StringVal("true"),
 			}),