diff --git a/pf/tests/provider_check_test.go b/pf/tests/provider_check_test.go index 529649928..07882f998 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/pf/tfbridge/provider_check.go b/pf/tfbridge/provider_check.go index 2f0dff87d..fb7cd1f94 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 310225d65..6cbdc3096 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/provider_test.go b/pkg/tfbridge/provider_test.go index dbe3b4487..195cb1ec5 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 diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go index 1a621ecda..0314ecc86 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 }