From 5d215c892096ab4b91c161de6c2d94f8f1772a17 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 1 Aug 2023 16:39:55 -0400 Subject: [PATCH] Permit computed default values to consult PriorState of the resource (#1318) 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. --- pf/tests/provider_check_test.go | 63 +++++++++++++++++++++++---- pf/tfbridge/provider_check.go | 1 + pkg/tfbridge/info.go | 3 ++ pkg/tfbridge/provider_test.go | 76 +++++++++++++++++++++++++++++++++ pkg/tfbridge/schema.go | 39 +++++++++++------ 5 files changed, 159 insertions(+), 23 deletions(-) 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 }