Repository: pulumi/pulumi-terraform-bridge
Pull request #1318 (2023-08-01 15:49:24)

Permit computed default values to consult PriorState of the resource
With these changes, ComputedDefault machinery is now able to consult PriorState. This is a building block to enable AutoNaming to work in the Plugin Framework context where __defaults are not being tracked, and therefore AutoName machinery is called repeatedly, causing any entropy in the randomized name generation to rebuild the name for a resource during a routine update, which needs to be avoided as the names need to persist through updates to avoid needless replace plans. With PriorState exposed, the machinery can be modified to consult it to avoid re-running random generators. This is coming in a subsequent PR.

Tests added to cover pkg/v3 and pf versions of providers.

github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/68faa20..b3d6603) with merge commit d52072fda4e7d42f6ed41650b08c7911c3298d45
github-actions[bot]: Diff for [pulumi-random](https://github.com/pulumi-bot/pulumi-random/compare/ca0afa3..8a88eb5) with merge commit d52072fda4e7d42f6ed41650b08c7911c3298d45
github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/42bd370..9f2a748) with merge commit b2acef6643d4d103e0eb9147fa5e9945759fd92f
github-actions[bot]: Diff for [pulumi-azuread](https://github.com/pulumi-bot/pulumi-azuread/compare/067ec5c..b7fbb28) with merge commit a1ab15dd8db4782ac15e6ab321f410f6596e3b95

Patch:
diff --git a/pf/tfbridge/provider_check.go b/pf/tfbridge/provider_check.go
index 2f0dff87..fb7cd1f9 100644
--- a/pf/tfbridge/provider_check.go
+++ b/pf/tfbridge/provider_check.go
@@ -63,6 +63,7 @@ func (p *provider) CheckWithContext(
 			URN:        urn,
 			Properties: checkedInputs,
 			Seed:       randomSeed,
+			PriorState: priorState,
 		},
 		PropertyMap:    checkedInputs,
 		ProviderConfig: p.lastKnownProviderConfig,
diff --git a/pkg/tfbridge/info.go b/pkg/tfbridge/info.go
index 310225d6..6cbdc309 100644
--- a/pkg/tfbridge/info.go
+++ b/pkg/tfbridge/info.go
@@ -485,6 +485,9 @@ type ComputeDefaultOptions struct {
 	// Property map before computing the defaults.
 	Properties resource.PropertyMap
 
+	// Property map representing prior state, only set for non-Create Resource operations.
+	PriorState resource.PropertyMap
+
 	// The engine provides a stable seed useful for generating random values consistently. This guarantees, for
 	// example, that random values generated across "pulumi preview" and "pulumi up" in the same deployment are
 	// consistent. This currently is only available for resource changes.
diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go
index 1a621ecd..0314ecc8 100644
--- a/pkg/tfbridge/schema.go
+++ b/pkg/tfbridge/schema.go
@@ -282,20 +282,30 @@ func elemSchemas(sch shim.Schema, ps *SchemaInfo) (shim.Schema, *SchemaInfo) {
 }
 
 type conversionContext struct {
-	Instance       *PulumiResource
-	ProviderConfig resource.PropertyMap
-	ApplyDefaults  bool
-	Assets         AssetTable
+	ComputeDefaultOptions ComputeDefaultOptions
+	ProviderConfig        resource.PropertyMap
+	ApplyDefaults         bool
+	Assets                AssetTable
 }
 
 func MakeTerraformInputs(instance *PulumiResource, config resource.PropertyMap, olds, news resource.PropertyMap,
 	tfs shim.SchemaMap, ps map[string]*SchemaInfo) (map[string]interface{}, AssetTable, error) {
 
+	cdOptions := ComputeDefaultOptions{}
+	if instance != nil {
+		cdOptions = ComputeDefaultOptions{
+			PriorState: olds,
+			Properties: instance.Properties,
+			Seed:       instance.Seed,
+			URN:        instance.URN,
+		}
+	}
+
 	ctx := &conversionContext{
-		Instance:       instance,
-		ProviderConfig: config,
-		ApplyDefaults:  true,
-		Assets:         AssetTable{},
+		ComputeDefaultOptions: cdOptions,
+		ProviderConfig:        config,
+		ApplyDefaults:         true,
+		Assets:                AssetTable{},
 	}
 	inputs, err := ctx.MakeTerraformInputs(olds, news, tfs, ps, false)
 	if err != nil {
@@ -689,17 +699,18 @@ func (ctx *conversionContext) applyDefaults(result map[string]interface{}, olds,
 					// Getting the correct context needs to refactor public methods such as
 					// MakeTerraformInput to MakeTerraformInputWithContext.
 					context.TODO(),
-					ComputeDefaultOptions{
-						URN:        ctx.Instance.URN,
-						Properties: ctx.Instance.Properties,
-						Seed:       ctx.Instance.Seed,
-					})
+					ctx.ComputeDefaultOptions,
+				)
 				if err != nil {
 					return err
 				}
 				defaultValue, source = v, "func"
 			} else if from := info.Default.From; from != nil {
-				v, err := from(ctx.Instance)
+				v, err := from(&PulumiResource{
+					URN:        ctx.ComputeDefaultOptions.URN,
+					Properties: ctx.ComputeDefaultOptions.Properties,
+					Seed:       ctx.ComputeDefaultOptions.Seed,
+				})
 				if err != nil {
 					return err
 				}


Test patch:
diff --git a/pf/tests/provider_check_test.go b/pf/tests/provider_check_test.go
index 52964992..07882f99 100644
--- a/pf/tests/provider_check_test.go
+++ b/pf/tests/provider_check_test.go
@@ -38,7 +38,10 @@ func TestCheck(t *testing.T) {
 		schema      schema.Schema
 		replay      string
 		replayMulti string
-		callback    tfbridge0.PreCheckCallback
+
+		callback tfbridge0.PreCheckCallback
+
+		customizeResource func(*tfbridge0.ResourceInfo)
 	}
 
 	testCases := []testCase{
@@ -257,6 +260,44 @@ func TestCheck(t *testing.T) {
 				return config, nil
 			},
 		},
+		{
+			name: "default application can consult prior state",
+			schema: schema.Schema{
+				Attributes: map[string]schema.Attribute{
+					"id": schema.StringAttribute{Computed: true},
+					"s":  schema.StringAttribute{Optional: true},
+				},
+			},
+			customizeResource: func(info *tfbridge0.ResourceInfo) {
+				info.Fields["s"] = &tfbridge0.SchemaInfo{
+					Default: &tfbridge0.DefaultInfo{
+						ComputeDefault: func(
+							_ context.Context,
+							opts tfbridge0.ComputeDefaultOptions,
+						) (any, error) {
+							return opts.PriorState["s"].StringValue(), nil
+						},
+					},
+				}
+			},
+			replay: `
+			{
+			  "method": "/pulumirpc.ResourceProvider/Check",
+			  "request": {
+			    "urn": "urn:pulumi:st::pg::testprovider:index/res:Res::r",
+			    "olds": {
+                               "s": "oldString"
+                            },
+			    "news": {},
+			    "randomSeed": "wqZZaHWVfsS1ozo3bdauTfZmjslvWcZpUjn7BzpS79c="
+			  },
+			  "response": {
+			    "inputs": {
+                              "s": "oldString"
+                            }
+			  }
+			}`,
+		},
 	}
 
 	for _, tc := range testCases {
@@ -278,23 +319,27 @@ func TestCheck(t *testing.T) {
 					ResourceSchema: tc.schema,
 				}},
 			}
+			res := tfbridge0.ResourceInfo{
+				Tok: "testprovider:index/res:Res",
+				Docs: &tfbridge0.DocInfo{
+					Markdown: []byte("OK"),
+				},
+				PreCheckCallback: tc.callback,
+				Fields:           map[string]*tfbridge0.SchemaInfo{},
+			}
+			if tc.customizeResource != nil {
+				tc.customizeResource(&res)
+			}
 			info := tfbridge0.ProviderInfo{
 				Name:         "testprovider",
 				P:            tfbridge.ShimProvider(testProvider),
 				Version:      "0.0.1",
 				MetadataInfo: &tfbridge0.MetadataInfo{},
 				Resources: map[string]*tfbridge0.ResourceInfo{
-					"testprovider_res": {
-						Tok: "testprovider:index/res:Res",
-						Docs: &tfbridge0.DocInfo{
-							Markdown: []byte("OK"),
-						},
-						PreCheckCallback: tc.callback,
-					},
+					"testprovider_res": &res,
 				},
 			}
 			s := newProviderServer(t, info)
-
 			if tc.replay != "" {
 				testutils.Replay(t, s, tc.replay)
 			}
diff --git a/pkg/tfbridge/provider_test.go b/pkg/tfbridge/provider_test.go
index dbe3b448..195cb1ec 100644
--- a/pkg/tfbridge/provider_test.go
+++ b/pkg/tfbridge/provider_test.go
@@ -817,6 +817,82 @@ func TestProviderReadNestedSecretV2(t *testing.T) {
 	testProviderReadNestedSecret(t, provider, "NestedSecretResource")
 }
 
+func TestCheck(t *testing.T) {
+	t.Run("Default application can consult prior state in Check", func(t *testing.T) {
+		provider := &Provider{
+			tf:     shimv2.NewProvider(testTFProviderV2),
+			config: shimv2.NewSchemaMap(testTFProviderV2.Schema),
+		}
+		computeStringDefault := func(_ context.Context, opts ComputeDefaultOptions) (interface{}, error) {
+			if v, ok := opts.PriorState["stringPropertyValue"]; ok {
+				return v.StringValue() + "!", nil
+			}
+			return nil, nil
+		}
+		provider.resources = map[tokens.Type]Resource{
+			"ExampleResource": {
+				TF:     shimv2.NewResource(testTFProviderV2.ResourcesMap["example_resource"]),
+				TFName: "example_resource",
+				Schema: &ResourceInfo{
+					Tok: "ExampleResource",
+					Fields: map[string]*SchemaInfo{
+						"string_property_value": {
+							Default: &DefaultInfo{
+								ComputeDefault: computeStringDefault,
+							},
+						},
+					},
+				},
+			},
+		}
+		testutils.Replay(t, provider, `
+		{
+		  "method": "/pulumirpc.ResourceProvider/Check",
+		  "request": {
+		    "urn": "urn:pulumi:dev::teststack::ExampleResource::exres",
+		    "randomSeed": "ZCiVOcvG/CT5jx4XriguWgj2iMpQEb8P3ZLqU/AS2yg=",
+		    "olds": {
+                      "__defaults": [],
+		     "stringPropertyValue": "oldString"
+		    },
+		    "news": {
+		      "arrayPropertyValues": []
+		    }
+		  },
+		  "response": {
+		    "inputs": {
+                      "__defaults": ["stringPropertyValue"],
+		      "arrayPropertyValues": [],
+		      "stringPropertyValue": "oldString!"
+		    }
+		  }
+		}
+                `)
+		// If old value is missing it is ignored.
+		testutils.Replay(t, provider, `
+		{
+		  "method": "/pulumirpc.ResourceProvider/Check",
+		  "request": {
+		    "urn": "urn:pulumi:dev::teststack::ExampleResource::exres",
+		    "randomSeed": "ZCiVOcvG/CT5jx4XriguWgj2iMpQEb8P3ZLqU/AS2yg=",
+		    "olds": {
+                      "__defaults": []
+		    },
+		    "news": {
+		      "arrayPropertyValues": []
+		    }
+		  },
+		  "response": {
+		    "inputs": {
+                      "__defaults": [],
+		      "arrayPropertyValues": []
+		    }
+		  }
+		}
+                `)
+	})
+}
+
 func TestCheckConfig(t *testing.T) {
 	t.Run("minimal", func(t *testing.T) {
 		// Ensure the method is minimally implemented. Pulumi will be passing a provider version. Make sure it