diff --git a/pf/internal/schemashim/provider.go b/pf/internal/schemashim/provider.go index 6ace43016..72366c1fe 100644 --- a/pf/internal/schemashim/provider.go +++ b/pf/internal/schemashim/provider.go @@ -170,3 +170,7 @@ func (p *SchemaOnlyProvider) NewResourceConfig(context.Context, map[string]inter func (p *SchemaOnlyProvider) IsSet(context.Context, interface{}) ([]interface{}, bool) { panic("schemaOnlyProvider does not implement runtime operation IsSet") } + +func (p *SchemaOnlyProvider) SupportsUnknownCollections() bool { + return true +} diff --git a/pf/proto/unsupported.go b/pf/proto/unsupported.go index b4168e830..2e3692b7d 100644 --- a/pf/proto/unsupported.go +++ b/pf/proto/unsupported.go @@ -82,3 +82,7 @@ func (Provider) NewResourceConfig(ctx context.Context, object map[string]interfa func (Provider) IsSet(ctx context.Context, v interface{}) ([]interface{}, bool) { panic("Unimplemented") } + +func (Provider) SupportsUnknownCollections() bool { + panic("Unimplemented") +} diff --git a/pkg/tests/internal/pulcheck/pulcheck.go b/pkg/tests/internal/pulcheck/pulcheck.go index c041a6b83..e0e2e61df 100644 --- a/pkg/tests/internal/pulcheck/pulcheck.go +++ b/pkg/tests/internal/pulcheck/pulcheck.go @@ -29,6 +29,26 @@ import ( "gotest.tools/assert" ) +func propNeedsUpdate(prop *schema.Schema) bool { + if prop.Computed && !prop.Optional { + return false + } + if prop.ForceNew { + return false + } + return true +} + +func resourceNeedsUpdate(res *schema.Resource) bool { + // If any of the properties need an update, then the resource needs an update. + for _, s := range res.Schema { + if propNeedsUpdate(s) { + return true + } + } + return false +} + // This is an experimental API. func EnsureProviderValid(t T, tfp *schema.Provider) { for _, r := range tfp.ResourcesMap { @@ -54,10 +74,12 @@ func EnsureProviderValid(t T, tfp *schema.Provider) { } } - r.UpdateContext = func( - ctx context.Context, rd *schema.ResourceData, i interface{}, - ) diag.Diagnostics { - return diag.Diagnostics{} + if resourceNeedsUpdate(r) && r.UpdateContext == nil { + r.UpdateContext = func( + ctx context.Context, rd *schema.ResourceData, i interface{}, + ) diag.Diagnostics { + return diag.Diagnostics{} + } } } require.NoError(t, tfp.InternalValidate()) diff --git a/pkg/tests/schema_pulumi_test.go b/pkg/tests/schema_pulumi_test.go index 3b17a109d..09246e822 100644 --- a/pkg/tests/schema_pulumi_test.go +++ b/pkg/tests/schema_pulumi_test.go @@ -852,9 +852,7 @@ resources: [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/test:Test: (create) [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - tests : [ - [0]: {} - ] + tests : output Resources: + 3 to create `), @@ -866,11 +864,12 @@ Resources: ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ tests: [ - ~ [0]: { - - testProp: "known_val" - } + - tests: [ + - [0]: { + - testProp: "known_val" + } ] + + tests: output Resources: + 1 to create ~ 1 to update @@ -1012,9 +1011,7 @@ resources: [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + prov:index/nestedTest:NestedTest: (create) [urn=urn:pulumi:test::test::prov:index/nestedTest:NestedTest::mainRes] - tests : [ - [0]: {} - ] + tests : output Resources: + 3 to create `), @@ -1026,24 +1023,18 @@ Resources: ~ prov:index/nestedTest:NestedTest: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/nestedTest:NestedTest::mainRes] - ~ tests: [ - ~ [0]: { - - nestedProps: [ - - [0]: { - - testProps: [ - - [0]: "known_val" - ] - } - ] - - nestedProps: [ - - [0]: { - - testProps: [ - - [0]: "known_val" - ] - } - ] - } + - tests: [ + - [0]: { + - nestedProps: [ + - [0]: { + - testProps: [ + - [0]: "known_val" + ] + } + ] + } ] + + tests: output Resources: + 1 to create ~ 1 to update @@ -1134,9 +1125,7 @@ resources: [urn=urn:pulumi:test::test::prov:index/nestedTest:NestedTest::mainRes] tests : [ [0]: { - nestedProps: [ - [0]: {} - ] + nestedProps: output } ] Resources: @@ -1152,16 +1141,14 @@ Resources: [urn=urn:pulumi:test::test::prov:index/nestedTest:NestedTest::mainRes] ~ tests: [ ~ [0]: { - ~ nestedProps: [ - ~ [0]: { - - testProps: [ - - [0]: "known_val" - ] - - testProps: [ - - [0]: "known_val" - ] - } + - nestedProps: [ + - [0]: { + - testProps: [ + - [0]: "known_val" + ] + } ] + + nestedProps: output } ] Resources: @@ -1262,9 +1249,7 @@ resources: [0]: { nestedProps: [ [0]: { - testProps : [ - [0]: output - ] + testProps : output } ] } @@ -1284,9 +1269,10 @@ Resources: ~ [0]: { ~ nestedProps: [ ~ [0]: { - ~ testProps: [ - ~ [0]: "known_val" => output + - testProps: [ + - [0]: "known_val" ] + + testProps: output } ] } diff --git a/pkg/tfbridge/diff_test.go b/pkg/tfbridge/diff_test.go index b6a61f599..45219d31a 100644 --- a/pkg/tfbridge/diff_test.go +++ b/pkg/tfbridge/diff_test.go @@ -88,7 +88,7 @@ func TestCustomizeDiff(t *testing.T) { Schema: &ResourceInfo{Fields: info}, } tfState, err := makeTerraformStateWithOpts(ctx, r, "id", stateMap, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{defaultZeroSchemaVersion: true, unknownCollectionsSupported: true}) assert.NoError(t, err) config, _, err := MakeTerraformConfig(ctx, &Provider{tf: provider}, inputsMap, sch, info) @@ -130,7 +130,7 @@ func TestCustomizeDiff(t *testing.T) { Schema: &ResourceInfo{Fields: info}, } tfState, err := makeTerraformStateWithOpts(ctx, r, "id", stateMap, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{defaultZeroSchemaVersion: true, unknownCollectionsSupported: true}) assert.NoError(t, err) config, _, err := MakeTerraformConfig(ctx, &Provider{tf: provider}, inputsMap, sch, info) @@ -184,7 +184,7 @@ func TestCustomizeDiff(t *testing.T) { Schema: &ResourceInfo{Fields: info}, } tfState, err := makeTerraformStateWithOpts(ctx, r, "id", stateMap, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{defaultZeroSchemaVersion: true, unknownCollectionsSupported: true}) assert.NoError(t, err) config, _, err := MakeTerraformConfig(ctx, &Provider{tf: provider}, inputsMap, sch, info) @@ -289,7 +289,8 @@ func diffTest(t *testing.T, tfs map[string]*v2Schema.Schema, inputs, sch, r, provider, info := s.setup(tfs) tfState, err := makeTerraformStateWithOpts(ctx, r, "id", stateMap, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: provider.SupportsUnknownCollections()}) assert.NoError(t, err) config, _, err := MakeTerraformConfig(ctx, &Provider{tf: provider}, inputsMap, sch, info) @@ -318,7 +319,8 @@ func diffTest(t *testing.T, tfs map[string]*v2Schema.Schema, inputs, t.Run("withIgnoreAllExpected", func(t *testing.T) { sch, r, provider, info := s.setup(tfs) tfState, err := makeTerraformStateWithOpts(ctx, r, "id", stateMap, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: provider.SupportsUnknownCollections()}) assert.NoError(t, err) config, _, err := MakeTerraformConfig(ctx, &Provider{tf: provider}, inputsMap, sch, info) @@ -1361,7 +1363,7 @@ func TestComputedListUpdate(t *testing.T) { "prop": []interface{}{"foo"}, "outp": "bar", }, - map[string]DiffKind{ + map[string]pulumirpc.PropertyDiff_Kind{ "prop": U, }, pulumirpc.DiffResponse_DIFF_SOME) diff --git a/pkg/tfbridge/internal/schemaconvert/schemaconvert.go b/pkg/tfbridge/internal/schemaconvert/schemaconvert.go new file mode 100644 index 000000000..95effb2e1 --- /dev/null +++ b/pkg/tfbridge/internal/schemaconvert/schemaconvert.go @@ -0,0 +1,134 @@ +package schemaconvert + +import ( + v1Schema "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + v2Schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" +) + +func Sdkv2ToV1Type(t v2Schema.ValueType) v1Schema.ValueType { + return v1Schema.ValueType(t) +} + +func Sdkv2ToV1SchemaOrResource(elem interface{}) interface{} { + switch elem := elem.(type) { + case nil: + return nil + case *v2Schema.Schema: + return Sdkv2ToV1Schema(elem) + case *v2Schema.Resource: + return Sdkv2ToV1Resource(elem) + default: + contract.Failf("unexpected type %T", elem) + return nil + } +} + +func Sdkv2ToV1Resource(sch *v2Schema.Resource) *v1Schema.Resource { + //nolint:staticcheck + if sch.MigrateState != nil { + contract.Failf("MigrateState is not supported in conversion") + } + if sch.StateUpgraders != nil { + contract.Failf("StateUpgraders is not supported in conversion") + } + //nolint:staticcheck + if sch.Create != nil || sch.Read != nil || sch.Update != nil || sch.Delete != nil || sch.Exists != nil || + sch.CreateContext != nil || sch.ReadContext != nil || sch.UpdateContext != nil || + sch.DeleteContext != nil || sch.Importer != nil { + contract.Failf("runtime methods not supported in conversion") + } + + if sch.CustomizeDiff != nil { + contract.Failf("CustomizeDiff is not supported in conversion") + } + + timeouts := v1Schema.ResourceTimeout{} + if sch.Timeouts != nil { + timeouts = v1Schema.ResourceTimeout{ + Create: sch.Timeouts.Create, + Read: sch.Timeouts.Read, + Update: sch.Timeouts.Update, + Delete: sch.Timeouts.Delete, + Default: sch.Timeouts.Default, + } + } + timoutsPtr := &timeouts + if sch.Timeouts == nil { + timoutsPtr = nil + } + + return &v1Schema.Resource{ + Schema: Sdkv2ToV1SchemaMap(sch.Schema), + SchemaVersion: sch.SchemaVersion, + DeprecationMessage: sch.DeprecationMessage, + Timeouts: timoutsPtr, + } +} + +func Sdkv2ToV1Schema(sch *v2Schema.Schema) *v1Schema.Schema { + if sch.DiffSuppressFunc != nil { + contract.Failf("DiffSuppressFunc is not supported in conversion") + } + + defaultFunc := v1Schema.SchemaDefaultFunc(nil) + if sch.DefaultFunc != nil { + defaultFunc = func() (interface{}, error) { + return sch.DefaultFunc() + } + } + + stateFunc := v1Schema.SchemaStateFunc(nil) + if sch.StateFunc != nil { + stateFunc = func(i interface{}) string { + return sch.StateFunc(i) + } + } + + set := v1Schema.SchemaSetFunc(nil) + if sch.Set != nil { + set = func(i interface{}) int { + return sch.Set(i) + } + } + + validateFunc := v1Schema.SchemaValidateFunc(nil) + if sch.ValidateFunc != nil { + validateFunc = func(i interface{}, s string) ([]string, []error) { + return sch.ValidateFunc(i, s) + } + } + + return &v1Schema.Schema{ + Type: Sdkv2ToV1Type(sch.Type), + Optional: sch.Optional, + Required: sch.Required, + Default: sch.Default, + DefaultFunc: defaultFunc, + Description: sch.Description, + InputDefault: sch.InputDefault, + Computed: sch.Computed, + ForceNew: sch.ForceNew, + StateFunc: stateFunc, + Elem: Sdkv2ToV1SchemaOrResource(sch.Elem), + MaxItems: sch.MaxItems, + MinItems: sch.MinItems, + Set: set, + //nolint:staticcheck + ComputedWhen: sch.ComputedWhen, + ConflictsWith: sch.ConflictsWith, + ExactlyOneOf: sch.ExactlyOneOf, + AtLeastOneOf: sch.AtLeastOneOf, + Deprecated: sch.Deprecated, + ValidateFunc: validateFunc, + Sensitive: sch.Sensitive, + } +} + +func Sdkv2ToV1SchemaMap(sch map[string]*v2Schema.Schema) map[string]*v1Schema.Schema { + res := make(map[string]*v1Schema.Schema) + for k, v := range sch { + res[k] = Sdkv2ToV1Schema(v) + } + return res +} diff --git a/pkg/tfbridge/provider.go b/pkg/tfbridge/provider.go index f30a1119c..0279ff55d 100644 --- a/pkg/tfbridge/provider.go +++ b/pkg/tfbridge/provider.go @@ -665,7 +665,8 @@ func buildTerraformConfig(ctx context.Context, p *Provider, vars resource.Proper } } - inputs, _, err := MakeTerraformInputs(ctx, nil, tfVars, nil, tfVars, p.config, p.info.Config) + inputs, _, err := makeTerraformInputsWithOptions(ctx, nil, tfVars, nil, tfVars, p.config, p.info.Config, + makeTerraformInputsOptions{UnknownCollectionsSupported: p.tf.SupportsUnknownCollections()}) if err != nil { return nil, err } @@ -971,7 +972,7 @@ func (p *Provider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (*pul inputs, _, err := makeTerraformInputsWithOptions(ctx, &PulumiResource{URN: urn, Properties: news, Seed: req.RandomSeed}, p.configValues, olds, news, schemaMap, res.Schema.Fields, - makeTerraformInputsOptions{DisableTFDefaults: true}) + makeTerraformInputsOptions{DisableTFDefaults: true, UnknownCollectionsSupported: p.tf.SupportsUnknownCollections()}) if err != nil { return nil, err } @@ -987,9 +988,10 @@ func (p *Provider) Check(ctx context.Context, req *pulumirpc.CheckRequest) (*pul failures = append(failures, p.adaptCheckFailures(ctx, urn, false /*isProvider*/, schemaMap, schemaInfos, errs)...) // Now re-generate the inputs WITH the TF defaults - inputs, assets, err := MakeTerraformInputs(ctx, + inputs, assets, err := makeTerraformInputsWithOptions(ctx, &PulumiResource{URN: urn, Properties: news, Seed: req.RandomSeed}, - p.configValues, olds, news, schemaMap, res.Schema.Fields) + p.configValues, olds, news, schemaMap, res.Schema.Fields, + makeTerraformInputsOptions{UnknownCollectionsSupported: p.tf.SupportsUnknownCollections()}) if err != nil { return nil, err } @@ -1073,7 +1075,10 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum } state, err := makeTerraformStateWithOpts(ctx, res, req.GetId(), olds, - makeTerraformStateOptions{defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion}, //nolint: gosimple + makeTerraformStateOptions{ + defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion, + unknownCollectionsSupported: p.tf.SupportsUnknownCollections(), + }, ) if err != nil { return nil, errors.Wrapf(err, "unmarshaling %s's instance state", urn) @@ -1347,7 +1352,10 @@ func (p *Provider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pulum return nil, err } state, err := unmarshalTerraformStateWithOpts(ctx, res, id, req.GetProperties(), fmt.Sprintf("%s.state", label), - unmarshalTerraformStateOptions{defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion}) //nolint: gosimple + unmarshalTerraformStateOptions{ + defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion, + unknownCollectionsSupported: p.tf.SupportsUnknownCollections(), + }) if err != nil { return nil, errors.Wrapf(err, "unmarshaling %s's instance state", urn) } @@ -1462,7 +1470,10 @@ func (p *Provider) Update(ctx context.Context, req *pulumirpc.UpdateRequest) (*p } state, err := makeTerraformStateWithOpts(ctx, res, req.GetId(), olds, - makeTerraformStateOptions{defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion}) //nolint: gosimple + makeTerraformStateOptions{ + defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion, + unknownCollectionsSupported: p.tf.SupportsUnknownCollections(), + }) if err != nil { return nil, errors.Wrapf(err, "unmarshaling %s's instance state", urn) } @@ -1592,7 +1603,10 @@ func (p *Provider) Delete(ctx context.Context, req *pulumirpc.DeleteRequest) (*p } // Fetch the resource attributes since many providers need more than just the ID to perform the delete. state, err := unmarshalTerraformStateWithOpts(ctx, res, req.GetId(), req.GetProperties(), label, - unmarshalTerraformStateOptions{defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion}) //nolint: gosimple + unmarshalTerraformStateOptions{ + defaultZeroSchemaVersion: opts.defaultZeroSchemaVersion, + unknownCollectionsSupported: p.tf.SupportsUnknownCollections(), + }) if err != nil { return nil, err } @@ -1646,13 +1660,14 @@ func (p *Provider) Invoke(ctx context.Context, req *pulumirpc.InvokeRequest) (*p // First, create the inputs. tfname := ds.TFName - inputs, _, err := MakeTerraformInputs( + inputs, _, err := makeTerraformInputsWithOptions( ctx, &PulumiResource{Properties: args}, p.configValues, nil, args, ds.TF.Schema(), - ds.Schema.Fields) + ds.Schema.Fields, + makeTerraformInputsOptions{UnknownCollectionsSupported: p.tf.SupportsUnknownCollections()}) if err != nil { return nil, errors.Wrapf(err, "couldn't prepare resource %v input state", tfname) } diff --git a/pkg/tfbridge/provider_test.go b/pkg/tfbridge/provider_test.go index f98ce692b..6a8dde072 100644 --- a/pkg/tfbridge/provider_test.go +++ b/pkg/tfbridge/provider_test.go @@ -4762,8 +4762,7 @@ func TestUnknowns(t *testing.T) { }, "response": { "properties":{ - "id":"", - "setBlockProps":[{"prop":""}] + "id":"" } } }`) @@ -4845,8 +4844,7 @@ func TestUnknowns(t *testing.T) { }, "response": { "properties":{ - "id":"", - "listBlockProps":[null] + "id":"" } } }`) @@ -4932,7 +4930,7 @@ func TestUnknowns(t *testing.T) { "response": { "properties":{ "id":"", - "nestedListBlockProps":[{"nestedProps":[null]}] + "nestedListBlockProps":[{"nestedProps":"04da6b54-80e4-46f7-96ec-b56ff0331ba9"}] } } }`) @@ -4973,8 +4971,7 @@ func TestUnknowns(t *testing.T) { }, "response": { "properties":{ - "id":"", - "nestedListBlockProps":[null] + "id":"" } } }`) @@ -5167,7 +5164,6 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { }) t.Run("unknown for set block prop", func(t *testing.T) { - // TODO[pulumi/pulumi-terraform-bridge#1885] testutils.Replay(t, provider, ` { "method": "/pulumirpc.ResourceProvider/Create", @@ -5197,7 +5193,6 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { }) t.Run("unknown for set block prop collection", func(t *testing.T) { - // TODO[pulumi/pulumi-terraform-bridge#1885] testutils.Replay(t, provider, ` { "method": "/pulumirpc.ResourceProvider/Create", @@ -5217,7 +5212,7 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { "listProps":null, "nestedListProps":null, "maxItemsOneProp":null, - "setBlockProps":[{"prop":""}], + "setBlockProps":"04da6b54-80e4-46f7-96ec-b56ff0331ba9", "listBlockProps":[], "nestedListBlockProps":[], "maxItemsOneBlockProp":null @@ -5319,7 +5314,6 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { }) t.Run("unknown for list block prop collection", func(t *testing.T) { - // TODO[pulumi/pulumi-terraform-bridge#1885] testutils.Replay(t, provider, ` { "method": "/pulumirpc.ResourceProvider/Create", @@ -5340,7 +5334,7 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { "nestedListProps":null, "maxItemsOneProp":null, "setBlockProps":[], - "listBlockProps":[{ "prop": null }], + "listBlockProps":"04da6b54-80e4-46f7-96ec-b56ff0331ba9", "nestedListBlockProps":[], "maxItemsOneBlockProp":null } @@ -5444,7 +5438,6 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { }) t.Run("unknown for nested list block prop nested collection", func(t *testing.T) { - // TODO[pulumi/pulumi-terraform-bridge#1885] testutils.Replay(t, provider, ` { "method": "/pulumirpc.ResourceProvider/Create", @@ -5467,7 +5460,7 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { "setBlockProps":[], "listBlockProps":[], "nestedListBlockProps":[{ - "nestedProps": [{"prop":null}] + "nestedProps": "04da6b54-80e4-46f7-96ec-b56ff0331ba9" }], "maxItemsOneBlockProp":null } @@ -5506,7 +5499,6 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { }) t.Run("unknown for nested list block collection", func(t *testing.T) { - // TODO[pulumi/pulumi-terraform-bridge#1885] testutils.Replay(t, provider, ` { "method": "/pulumirpc.ResourceProvider/Create", @@ -5528,7 +5520,7 @@ func TestPlanResourceChangeUnknowns(t *testing.T) { "maxItemsOneProp":null, "setBlockProps":[], "listBlockProps":[], - "nestedListBlockProps":[{"nestedProps":[]}], + "nestedListBlockProps":"04da6b54-80e4-46f7-96ec-b56ff0331ba9", "maxItemsOneBlockProp":null } } diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go index 43708f0fe..e42b79e5c 100644 --- a/pkg/tfbridge/schema.go +++ b/pkg/tfbridge/schema.go @@ -287,18 +287,20 @@ func elemSchemas(sch shim.Schema, ps *SchemaInfo) (shim.Schema, *SchemaInfo) { } type conversionContext struct { - Ctx context.Context - ComputeDefaultOptions ComputeDefaultOptions - ProviderConfig resource.PropertyMap - ApplyDefaults bool - ApplyTFDefaults bool - ApplyMaxItemsOneDefaults bool - Assets AssetTable + Ctx context.Context + ComputeDefaultOptions ComputeDefaultOptions + ProviderConfig resource.PropertyMap + ApplyDefaults bool + ApplyTFDefaults bool + ApplyMaxItemsOneDefaults bool + Assets AssetTable + UnknownCollectionsSupported bool } type makeTerraformInputsOptions struct { - DisableDefaults bool - DisableTFDefaults bool + DisableDefaults bool + DisableTFDefaults bool + UnknownCollectionsSupported bool } func makeTerraformInputsWithOptions( @@ -317,12 +319,13 @@ func makeTerraformInputsWithOptions( } cctx := &conversionContext{ - Ctx: ctx, - ComputeDefaultOptions: cdOptions, - ProviderConfig: config, - ApplyDefaults: !opts.DisableDefaults, - ApplyTFDefaults: !opts.DisableTFDefaults, - Assets: AssetTable{}, + Ctx: ctx, + ComputeDefaultOptions: cdOptions, + ProviderConfig: config, + ApplyDefaults: !opts.DisableDefaults, + ApplyTFDefaults: !opts.DisableTFDefaults, + Assets: AssetTable{}, + UnknownCollectionsSupported: opts.UnknownCollectionsSupported, } inputs, err := cctx.makeTerraformInputs(olds, news, tfs, ps) @@ -332,6 +335,7 @@ func makeTerraformInputsWithOptions( return inputs, cctx.Assets, err } +// Deprecated: missing some important functionality, use makeTerraformInputsWithOptions instead. func MakeTerraformInputs( ctx context.Context, instance *PulumiResource, config resource.PropertyMap, olds, news resource.PropertyMap, tfs shim.SchemaMap, ps map[string]*SchemaInfo, @@ -530,7 +534,7 @@ func (ctx *conversionContext) makeTerraformInput( // If any variables are unknown, we need to mark them in the inputs so the config map treats it right. This // requires the use of the special UnknownVariableValue sentinel in Terraform, which is how it internally stores // interpolated variables whose inputs are currently unknown. - return makeTerraformUnknown(tfs), nil + return makeTerraformUnknown(tfs, ctx.UnknownCollectionsSupported), nil default: contract.Failf("Unexpected value marshaled: %v", v) return nil, nil @@ -969,13 +973,13 @@ func makeTerraformUnknownElement(elem interface{}) interface{} { switch e := elem.(type) { case shim.Schema: // If the element uses a normal schema, defer to makeTerraformUnknown. - return makeTerraformUnknown(e) + return makeTerraformUnknown(e, false) case shim.Resource: // If the element uses a resource schema, fill in unknown values for any required properties. res := make(map[string]interface{}) e.Schema().Range(func(k string, v shim.Schema) bool { if v.Required() { - res[k] = makeTerraformUnknown(v) + res[k] = makeTerraformUnknown(v, false) } return true }) @@ -989,7 +993,10 @@ func makeTerraformUnknownElement(elem interface{}) interface{} { // // It is important that we use the TF schema (if available) to decide what shape the unknown value should have: // e.g. TF does not play nicely with unknown lists, instead expecting a list of unknowns. -func makeTerraformUnknown(tfs shim.Schema) interface{} { +func makeTerraformUnknown(tfs shim.Schema, unknownCollectionsSupported bool) interface{} { + if unknownCollectionsSupported { + return TerraformUnknownVariableValue + } if tfs == nil { return TerraformUnknownVariableValue } @@ -1233,7 +1240,8 @@ func MakeTerraformOutput( func MakeTerraformConfig(ctx context.Context, p *Provider, m resource.PropertyMap, tfs shim.SchemaMap, ps map[string]*SchemaInfo) (shim.ResourceConfig, AssetTable, error) { inputs, assets, err := makeTerraformInputsWithOptions(ctx, nil, p.configValues, nil, m, tfs, ps, - makeTerraformInputsOptions{DisableDefaults: true, DisableTFDefaults: true}) + makeTerraformInputsOptions{DisableDefaults: true, DisableTFDefaults: true, + UnknownCollectionsSupported: p.tf.SupportsUnknownCollections()}) if err != nil { return nil, nil, err } @@ -1241,7 +1249,7 @@ func MakeTerraformConfig(ctx context.Context, p *Provider, m resource.PropertyMa } // UnmarshalTerraformConfig creates a Terraform config map from a Pulumi RPC property map. -// Unused internally. +// Deprecated: use MakeTerraformConfig instead. func UnmarshalTerraformConfig(ctx context.Context, p *Provider, m *pbstruct.Struct, tfs shim.SchemaMap, ps map[string]*SchemaInfo, label string) (shim.ResourceConfig, AssetTable, error) { @@ -1289,7 +1297,8 @@ func MakeTerraformConfigFromInputs( } type makeTerraformStateOptions struct { - defaultZeroSchemaVersion bool + defaultZeroSchemaVersion bool + unknownCollectionsSupported bool } func makeTerraformStateWithOpts( @@ -1317,7 +1326,7 @@ func makeTerraformStateWithOpts( // ints, to represent numbers. inputs, _, err := makeTerraformInputsWithOptions(ctx, nil, nil, nil, m, res.TF.Schema(), res.Schema.Fields, makeTerraformInputsOptions{ - DisableDefaults: true, DisableTFDefaults: true, + DisableDefaults: true, DisableTFDefaults: true, UnknownCollectionsSupported: opts.unknownCollectionsSupported, }) if err != nil { return nil, err @@ -1329,7 +1338,7 @@ func makeTerraformStateWithOpts( // MakeTerraformState converts a Pulumi property bag into its Terraform equivalent. This requires // flattening everything and serializing individual properties as strings. This is a little awkward, but it's how // Terraform represents resource properties (schemas are simply sugar on top). -// Prefer makeTerraformStateWithOpts for internal use. +// Deprecated: Use makeTerraformStateWithOpts instead. func MakeTerraformState( ctx context.Context, res Resource, id string, m resource.PropertyMap, ) (shim.InstanceState, error) { @@ -1337,7 +1346,8 @@ func MakeTerraformState( } type unmarshalTerraformStateOptions struct { - defaultZeroSchemaVersion bool + defaultZeroSchemaVersion bool + unknownCollectionsSupported bool } func unmarshalTerraformStateWithOpts( @@ -1363,7 +1373,7 @@ func unmarshalTerraformStateWithOpts( } // UnmarshalTerraformState unmarshals a Terraform instance state from an RPC property map. -// Prefer unmarshalTerraformStateWithOpts for internal use. +// Deprecated: Use unmarshalTerraformStateWithOpts instead. func UnmarshalTerraformState( ctx context.Context, r Resource, id string, m *pbstruct.Struct, l string, ) (shim.InstanceState, error) { diff --git a/pkg/tfbridge/schema_test.go b/pkg/tfbridge/schema_test.go index a641b4783..329d385f7 100644 --- a/pkg/tfbridge/schema_test.go +++ b/pkg/tfbridge/schema_test.go @@ -47,14 +47,14 @@ func makeTerraformInputsNoDefaults(olds, news resource.PropertyMap, tfs shim.SchemaMap, ps map[string]*SchemaInfo, ) (map[string]interface{}, AssetTable, error) { return makeTerraformInputsWithOptions(context.Background(), nil, nil, olds, news, tfs, ps, - makeTerraformInputsOptions{DisableDefaults: true, DisableTFDefaults: true}) + makeTerraformInputsOptions{DisableDefaults: true, DisableTFDefaults: true, UnknownCollectionsSupported: true}) } func makeTerraformInputsForConfig(olds, news resource.PropertyMap, tfs shim.SchemaMap, ps map[string]*SchemaInfo, ) (map[string]interface{}, AssetTable, error) { return makeTerraformInputsWithOptions(context.Background(), nil, nil, olds, news, tfs, ps, - makeTerraformInputsOptions{}) + makeTerraformInputsOptions{UnknownCollectionsSupported: true}) } func makeTerraformInput(v resource.PropertyValue, tfs shim.Schema, ps *SchemaInfo) (interface{}, error) { @@ -689,7 +689,8 @@ func TestMetaProperties(t *testing.T) { state, err = makeTerraformStateWithOpts( ctx, Resource{TF: res, Schema: &ResourceInfo{}}, state.ID(), props, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: prov.SupportsUnknownCollections()}) assert.NoError(t, err) assert.NotNil(t, state) @@ -705,7 +706,8 @@ func TestMetaProperties(t *testing.T) { state, err = makeTerraformStateWithOpts( ctx, Resource{TF: res, Schema: &ResourceInfo{}}, state.ID(), props, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: prov.SupportsUnknownCollections()}) assert.NoError(t, err) assert.NotNil(t, state) @@ -745,7 +747,8 @@ func TestMetaProperties(t *testing.T) { state, err = makeTerraformStateWithOpts( ctx, Resource{TF: res, Schema: &ResourceInfo{}}, state.ID(), props, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: prov.SupportsUnknownCollections()}) assert.NoError(t, err) assert.NotNil(t, state) @@ -774,7 +777,8 @@ func TestInjectingCustomTimeouts(t *testing.T) { state, err = makeTerraformStateWithOpts( ctx, Resource{TF: res, Schema: &ResourceInfo{}}, state.ID(), props, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: prov.SupportsUnknownCollections()}) assert.NoError(t, err) assert.NotNil(t, state) @@ -790,7 +794,8 @@ func TestInjectingCustomTimeouts(t *testing.T) { state, err = makeTerraformStateWithOpts( ctx, Resource{TF: res, Schema: &ResourceInfo{}}, state.ID(), props, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: prov.SupportsUnknownCollections()}) assert.NoError(t, err) assert.NotNil(t, state) @@ -833,7 +838,8 @@ func TestInjectingCustomTimeouts(t *testing.T) { state, err = makeTerraformStateWithOpts( ctx, Resource{TF: res, Schema: &ResourceInfo{}}, state.ID(), props, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: prov.SupportsUnknownCollections()}) assert.NoError(t, err) assert.NotNil(t, state) @@ -890,7 +896,8 @@ func TestResultAttributesRoundTrip(t *testing.T) { state, err = makeTerraformStateWithOpts( ctx, Resource{TF: res, Schema: &ResourceInfo{}}, state.ID(), props, - makeTerraformStateOptions{defaultZeroSchemaVersion: true}) + makeTerraformStateOptions{ + defaultZeroSchemaVersion: true, unknownCollectionsSupported: prov.SupportsUnknownCollections()}) assert.NoError(t, err) assert.NotNil(t, state) @@ -3083,9 +3090,7 @@ func Test_makeTerraformInputsNoDefaults(t *testing.T) { // The string property inside Computed is irrelevant. "unknownArrayValue": resource.Computed{Element: resource.NewStringProperty("")}, }), - // NOTE: is this the behavior we would want here? Why is the result [unk] instead of unk? - //nolint:lll - expect: autogold.Expect(map[string]interface{}{"unknown_array_value": []interface{}{"74D93920-ED26-11E3-AC10-0800200C9A66"}}), + expect: autogold.Expect(map[string]interface{}{"unknown_array_value": "74D93920-ED26-11E3-AC10-0800200C9A66"}), }, { testCaseName: "unknown_object_value", diff --git a/pkg/tfshim/schema/provider.go b/pkg/tfshim/schema/provider.go index 8a303c420..db53d0921 100644 --- a/pkg/tfshim/schema/provider.go +++ b/pkg/tfshim/schema/provider.go @@ -123,3 +123,7 @@ func (ProviderShim) NewResourceConfig( func (ProviderShim) IsSet(ctx context.Context, v interface{}) ([]interface{}, bool) { panic("this provider is schema-only and does not support runtime operations") } + +func (ProviderShim) SupportsUnknownCollections() bool { + panic("this provider is schema-only and does not support runtime operations") +} diff --git a/pkg/tfshim/sdk-v1/provider.go b/pkg/tfshim/sdk-v1/provider.go index 7f5044f81..5b64e4ec7 100644 --- a/pkg/tfshim/sdk-v1/provider.go +++ b/pkg/tfshim/sdk-v1/provider.go @@ -177,3 +177,7 @@ func (p v1Provider) IsSet(_ context.Context, v interface{}) ([]interface{}, bool } return nil, false } + +func (p v1Provider) SupportsUnknownCollections() bool { + return false +} diff --git a/pkg/tfshim/sdk-v2/provider.go b/pkg/tfshim/sdk-v2/provider.go index d10f51eea..7ab87eb37 100644 --- a/pkg/tfshim/sdk-v2/provider.go +++ b/pkg/tfshim/sdk-v2/provider.go @@ -223,3 +223,7 @@ func (p v2Provider) IsSet(_ context.Context, v interface{}) ([]interface{}, bool } return nil, false } + +func (p v2Provider) SupportsUnknownCollections() bool { + return true +} diff --git a/pkg/tfshim/shim.go b/pkg/tfshim/shim.go index 3d0ca21e8..b6e814a80 100644 --- a/pkg/tfshim/shim.go +++ b/pkg/tfshim/shim.go @@ -250,6 +250,10 @@ type Provider interface { // Checks if a value is representing a Set, and unpacks its elements on success. IsSet(ctx context.Context, v interface{}) ([]interface{}, bool) + + // SupportsUnknownCollections returns false if the provider needs special handling of unknown collections. + // False for the sdkv1 provider. + SupportsUnknownCollections() bool } type TimeoutOptions struct { diff --git a/pkg/tfshim/tfplugin5/provider.go b/pkg/tfshim/tfplugin5/provider.go index a462f64a3..0255dd6cb 100644 --- a/pkg/tfshim/tfplugin5/provider.go +++ b/pkg/tfshim/tfplugin5/provider.go @@ -594,3 +594,7 @@ func (p *provider) IsSet(ctx context.Context, v interface{}) ([]interface{}, boo } return result, true } + +func (p *provider) SupportsUnknownCollections() bool { + return true +} diff --git a/pkg/tfshim/util/filter.go b/pkg/tfshim/util/filter.go index ea583ab07..c34fc91e6 100644 --- a/pkg/tfshim/util/filter.go +++ b/pkg/tfshim/util/filter.go @@ -141,6 +141,10 @@ func (p *FilteringProvider) IsSet(ctx context.Context, v interface{}) ([]interfa return p.Provider.IsSet(ctx, v) } +func (p *FilteringProvider) SupportsUnknownCollections() bool { + return p.Provider.SupportsUnknownCollections() +} + type filteringMap struct { inner shim.ResourceMap tokenFilter func(string) bool diff --git a/pkg/tfshim/util/util.go b/pkg/tfshim/util/util.go index d55dea758..c91fafb2b 100644 --- a/pkg/tfshim/util/util.go +++ b/pkg/tfshim/util/util.go @@ -103,3 +103,7 @@ func (UnimplementedProvider) NewResourceConfig( func (UnimplementedProvider) IsSet(ctx context.Context, v interface{}) ([]interface{}, bool) { panic("unimplemented") } + +func (UnimplementedProvider) SupportsUnknownCollections() bool { + panic("unimplemented") +}