From deb3c39bc9456f74ae44c4fc5025292a7b3e1c5d Mon Sep 17 00:00:00 2001 From: VenelinMartinov Date: Tue, 1 Oct 2024 18:08:03 +0100 Subject: [PATCH] Schema-aware pulumi-level detailed diff calculation in the SDKv2 bridge (#2405) This PR adds a new algorithm for calculating the detailed diff which acts on the pulumi property values for the SDKv2 bridge, comparing the planned state returned by `Diff` to the prior state. This is flagged under the existing `DiffEqualDecisionOverride` feature flag. The results look very promising so far - all the detailed diff integration tests pass and the issues previously reported are almost all fixed by this. ## Why ## The current detailed diff acts on the `InstanceDiff` structure which is internal to the plugin-sdk. This has a few shortcomings: - TF does not actually refer to this for the detailed diff, so it might point to diffs which are not present in TF. - It refers to TF attribute paths, which are tricky to translate back in some cases. - It does not compare the planned state with the prior state but compares the news vs olds - this misses properties added by TF planning. ## Implementation ## The new algorithm is under `pkg/tfbridge/detailed_diff.go` and used in `pkg/tfbridge/provider.go:Diff` only for the SDKv2 and only if the `DiffEqualDecisionOverride` is enabled. The main entrypoint is `makePulumiDetailedDiffV2` - which in turn calls `makePropDiff` on each property. That branches on the type of the property and we have a separate function responsible for calculating the detailed diff for each property type. There's a few interesting bits here: - We always walk the full tree even when comparing against a nil property and simplify the diff after in `simplifyDiff`. This is in order to get replaces correct. More on that later. - When returning the diff to the engine we only return the simplest possible diff which explains the changes. So instead of `prop: Update, prop.subprop: Add`, we only return `prop.subprop: Add`. This seems to work much better in the engine and goes around some weird behaviour in the detailed diff display (see https://github.com/pulumi/pulumi-terraform-bridge/issues/2234 and https://github.com/pulumi/pulumi-terraform-bridge/issues/2400). Moreover, the first can be inferred from the second, so there is no reason for the bridge to return the full tree if only a leaf was changed. - We can not correctly identify diffs between nil and empty collections because of how the TF SDKv2 works without additional work. This is studied in `TestNilVsEmptyListProperty` and `TestNilVsEmptyMapProperty` in `pkg/cross-tests/diff_cross_test.go`. This is probably fine for now and a full fix is not possible. We can make a partial fix for non-computed properties by inspecting the pulumi inputs, before the plan. - There's a bit of an edge case with Unknowns and Replaces - we might not have enough information to tell the user they'll get a replace because the property which causes the replaces is nested inside an unknown. There's not much to do here, except to choose which side to err on. The algorithm currently does not say there's a replace. ### On Replaces ### We do not short-circuit detailed diffs when comparing non-nil properties against nil ones. The reason for that is that a replace might be triggered by a `ForceNew` inside a nested property of a non-`ForceNew` property. We instead always walk the full tree even when comparing against a nil property. We then later do a simplification step for the detailed diff in `simplifyDiff` in order to reduce the diff to what the user expects to see. For example: This is a list of objects with two properties, one of which is `ForceNew` ``` schema = { "list_prop": { Type: List, Elem: { "prop": String "force_new_prop": StringWithForceNew } } } ``` We are diffing an unspecified list against a list with a single element ``` olds = {} news = { "list_prop": [ { "prop": "val", "force_new_prop" : "val" } ] ``` The user expects to see: ``` + list_prop ``` or because of how collections work in TF SDKv2 (see https://github.com/pulumi/pulumi-terraform-bridge/issues/2233) ``` + list_prop[0] ``` An element was added to the list. When calculating the detailed diff we can short-circuit the diff when comparing the two lists, as we can see they have different lengths. This would identify the correct element to be displayed to the user as the reason for the diff but would fail to identify the diff as a replace, since we never saw the `ForceNew` on the nested property `force_new_prop` of the list elements. That is why, instead of short-circuiting the diff, we walk the full tree down and compare every property against a nil if it is not specified on the other side. We then to a simplification pass over the detailed diff, which respects any replaces triggered by nested properties and bubbles these up. There is a full case study of the TF behaviour around replaces in `pkg/cross-tests/diff_cross_test.go` `TestAttributeCollectionForceNew`, `TestBlockCollectionForceNew`, `TestBlockCollectionElementForceNew`. ## Testing ## Unit tests for the detailed diff algorithm in `pkg/tfbridge/detailed_diff_test.go` - this tries to cover all meaningful permutations of schemas: - `TestBasicDetailedDiff` tests all the meaningful pairs between nil values, empty values, non-empty values and computed for each TF type. - `TestDetailedDiffObject`, `TestDetailedDiffList`, `TestDetailedDiffMap`, `TestDetailedDiffSet` covers the cases not covered above for object and collection types. - `TestDetailedDiffTFForceNewPlain`, `TestDetailedDiffTFForceNewAttributeCollection`, `TestDetailedDiffTFForceNewBlockCollection`, `TestDetailedDiffTFForceNewElemBlockCollection`, `TestDetailedDiffTFForceNewObject` cover `ForceNew` behaviour in all TF types. - `TestDetailedDiffPulumiSchemaOverride` covers pulumi schema overrides Integration tests in `pkg/tests/schema_pulumi_test.go`, mostly `TestDetailedDiffPlainTypes` and `TestUnknownBlocks`. Note that most of the edge cases and issues previously discovered here are resolved by this PR. ## Follow-up Work ## Not done but will follow-up in separate PRs: - Non-trivial set diffing - sets are currently diffed the same as lists, which has all the previous issues with set diffs. https://github.com/pulumi/pulumi-terraform-bridge/issues/2200 - Non-trivial list diffing - we can do something like https://github.com/pulumi/pulumi-terraform-bridge/issues/2295 here. Note that we still need to investigate how this interacts with ForceNew and how TF preserves or does not preserve list element identity. We likely need to respect that in order not to have confusing unexplained replaces caused by list element changes. ## Related Issues ## fixes: - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2294 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2296 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/1504 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/1895 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2141 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2235 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2325 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2400 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2234 - fixes https://github.com/pulumi/pulumi-terraform-bridge/issues/2427 does not fix: - https://github.com/pulumi/pulumi-terraform-bridge/issues/2399 - we must either fix the saved state to not contain redundant nils or fix the display logic in the engine to ignore these. - https://github.com/pulumi/pulumi-terraform-bridge/issues/2233 - This works the same as TF and seems to be a limitation of the SDKv2. --- pkg/tests/cross-tests/diff_check.go | 6 +- pkg/tests/cross-tests/diff_cross_test.go | 640 +++++++- pkg/tests/schema_pulumi_test.go | 456 ++---- pkg/tfbridge/detailed_diff.go | 419 ++++++ pkg/tfbridge/detailed_diff_test.go | 1755 ++++++++++++++++++++++ pkg/tfbridge/provider.go | 64 +- pkg/tfshim/sdk-v1/instance_diff.go | 5 + pkg/tfshim/sdk-v2/instance_diff.go | 5 + pkg/tfshim/sdk-v2/provider2.go | 11 + pkg/tfshim/shim.go | 2 + pkg/tfshim/tfplugin5/instance_diff.go | 4 + 11 files changed, 3025 insertions(+), 342 deletions(-) create mode 100644 pkg/tfbridge/detailed_diff.go create mode 100644 pkg/tfbridge/detailed_diff_test.go diff --git a/pkg/tests/cross-tests/diff_check.go b/pkg/tests/cross-tests/diff_check.go index 5973e856d..56a34a7e8 100644 --- a/pkg/tests/cross-tests/diff_check.go +++ b/pkg/tests/cross-tests/diff_check.go @@ -58,6 +58,7 @@ type diffResult struct { } func runDiffCheck(t T, tc diffTestCase) diffResult { + t.Helper() tfwd := t.TempDir() lifecycleArgs := lifecycleArgs{CreateBeforeDestroy: !tc.DeleteBeforeReplace} @@ -108,6 +109,7 @@ func runDiffCheck(t T, tc diffTestCase) diffResult { } func (tc *diffTestCase) verifyBasicDiffAgreement(t T, tfActions []string, us auto.UpdateSummary, diffResponse pulumiDiffResp) { + t.Helper() t.Logf("UpdateSummary.ResourceChanges: %#v", us.ResourceChanges) // Action list from https://github.com/opentofu/opentofu/blob/main/internal/plans/action.go#L11 if len(tfActions) == 0 { @@ -147,14 +149,14 @@ func (tc *diffTestCase) verifyBasicDiffAgreement(t T, tfActions []string, us aut rc := *us.ResourceChanges assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same") assert.Equalf(t, 1, rc[string(apitype.OpReplace)], "expected the test resource to get a replace plan") - assert.Equalf(t, diffResponse.DeleteBeforeReplace, false, "expected deleteBeforeReplace to be true") + assert.Equalf(t, false, diffResponse.DeleteBeforeReplace, "expected deleteBeforeReplace to be true") } else if tfActions[0] == "delete" && tfActions[1] == "create" { require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil") rc := *us.ResourceChanges t.Logf("UpdateSummary.ResourceChanges: %#v", rc) assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same") assert.Equalf(t, 1, rc[string(apitype.OpReplace)], "expected the test resource to get a replace plan") - assert.Equalf(t, diffResponse.DeleteBeforeReplace, true, "expected deleteBeforeReplace to be true") + assert.Equalf(t, true, diffResponse.DeleteBeforeReplace, "expected deleteBeforeReplace to be true") } else { panic("TODO: do not understand this TF action yet: " + fmt.Sprint(tfActions)) } diff --git a/pkg/tests/cross-tests/diff_cross_test.go b/pkg/tests/cross-tests/diff_cross_test.go index 6feeede30..8899bf56f 100644 --- a/pkg/tests/cross-tests/diff_cross_test.go +++ b/pkg/tests/cross-tests/diff_cross_test.go @@ -26,7 +26,6 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hexops/autogold/v2" "github.com/stretchr/testify/require" ) @@ -937,7 +936,644 @@ func TestMaxItemsOneCollectionOnlyDiff(t *testing.T) { return val["rule"].([]any)[0].(map[string]any)["filter"] } + t.Log(diff.PulumiDiff) require.Equal(t, []string{"update"}, diff.TFDiff.Actions) require.NotEqual(t, getFilter(diff.TFDiff.Before), getFilter(diff.TFDiff.After)) - autogold.Expect(map[string]interface{}{"rules[0].filter": map[string]interface{}{"kind": "UPDATE"}}).Equal(t, diff.PulumiDiff.DetailedDiff) + require.True(t, findKeyInPulumiDetailedDiff(diff.PulumiDiff.DetailedDiff, "rules[0].filter")) +} + +func TestNilVsEmptyListProperty(t *testing.T) { + cfgEmpty := map[string]any{"f0": []any{}} + cfgNil := map[string]any{} + + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "f0": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } + + t.Run("nil to empty", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: cfgNil, + Config2: cfgEmpty, + }) + + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) + + t.Run("empty to nil", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: cfgEmpty, + Config2: cfgNil, + }) + + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) +} + +func TestNilVsEmptyMapProperty(t *testing.T) { + cfgEmpty := map[string]any{"f0": map[string]any{}} + cfgNil := map[string]any{} + + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "f0": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } + + t.Run("nil to empty", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: cfgNil, + Config2: cfgEmpty, + }) + + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) + + t.Run("empty to nil", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: cfgEmpty, + Config2: cfgNil, + }) + + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) +} + +func findKindInPulumiDetailedDiff(detailedDiff map[string]interface{}, key string) bool { + for _, val := range detailedDiff { + // ADD is a valid kind but is the default value for kind + // This means that it is missed out from the representation + if key == "ADD" { + if len(val.(map[string]interface{})) == 0 { + return true + } + } + if val.(map[string]interface{})["kind"] == key { + return true + } + } + return false +} + +func findKeyInPulumiDetailedDiff(detailedDiff map[string]interface{}, key string) bool { + for k := range detailedDiff { + if k == key { + return true + } + } + return false +} + +func TestNilVsEmptyNestedCollections(t *testing.T) { + // TODO: remove once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + for _, MaxItems := range []int{0, 1} { + t.Run(fmt.Sprintf("MaxItems=%d", MaxItems), func(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list": { + Type: schema.TypeList, + Optional: true, + MaxItems: MaxItems, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "set": { + Type: schema.TypeSet, + Optional: true, + MaxItems: MaxItems, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + } + + t.Run("nil to empty list", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{}, + Config2: map[string]any{"list": []any{}}, + }) + + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) + + t.Run("nil to empty set", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{}, + Config2: map[string]any{"set": []any{}}, + }) + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) + + t.Run("empty to nil list", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{}}, + Config2: map[string]any{}, + }) + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) + + t.Run("empty to nil set", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{}}, + Config2: map[string]any{}, + }) + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) + + listOfStrType := tftypes.List{ElementType: tftypes.String} + + objType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "x": listOfStrType, + }, + } + + listType := tftypes.List{ElementType: objType} + + listVal := tftypes.NewValue( + listType, + []tftypes.Value{ + tftypes.NewValue( + objType, + map[string]tftypes.Value{ + "x": tftypes.NewValue(listOfStrType, + []tftypes.Value{}), + }, + ), + }, + ) + + listConfig := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list": listType, + }, + }, + map[string]tftypes.Value{ + "list": listVal, + }, + ) + + t.Run("nil to empty list in list", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{}, + Config2: listConfig, + }) + + require.Equal(t, []string{"update"}, diff.TFDiff.Actions) + require.NotEqual(t, diff.TFDiff.Before, diff.TFDiff.After) + require.True(t, findKindInPulumiDetailedDiff(diff.PulumiDiff.DetailedDiff, "ADD")) + }) + + t.Run("empty list in list to nil", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: listConfig, + Config2: map[string]any{}, + }) + + require.Equal(t, []string{"update"}, diff.TFDiff.Actions) + require.NotEqual(t, diff.TFDiff.Before, diff.TFDiff.After) + require.True(t, findKindInPulumiDetailedDiff(diff.PulumiDiff.DetailedDiff, "DELETE")) + }) + + setType := tftypes.Set{ElementType: objType} + + setVal := tftypes.NewValue( + setType, + []tftypes.Value{ + tftypes.NewValue( + objType, + map[string]tftypes.Value{ + "x": tftypes.NewValue(listOfStrType, + []tftypes.Value{}), + }, + ), + }, + ) + + setConfig := tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set": setType, + }, + }, + map[string]tftypes.Value{ + "set": setVal, + }, + ) + + t.Run("nil to empty list in set", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{}, + Config2: setConfig, + }) + + require.Equal(t, []string{"update"}, diff.TFDiff.Actions) + require.NotEqual(t, diff.TFDiff.Before, diff.TFDiff.After) + t.Log(diff.PulumiDiff.DetailedDiff) + require.True(t, findKindInPulumiDetailedDiff(diff.PulumiDiff.DetailedDiff, "ADD")) + }) + + t.Run("empty list in set to nil", func(t *testing.T) { + diff := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: setConfig, + Config2: map[string]any{}, + }) + + require.Equal(t, []string{"no-op"}, diff.TFDiff.Actions) + }) + }) + } +} + +func TestAttributeCollectionForceNew(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "set": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "map": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } + + t.Run("list", func(t *testing.T) { + t.Run("changed non-empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{"A"}}, + Config2: map[string]any{"list": []any{"B"}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE_REPLACE")) + }) + + t.Run("changed to empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{"A"}}, + Config2: map[string]any{"list": []any{}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "DELETE_REPLACE")) + }) + + t.Run("changed from empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{}}, + Config2: map[string]any{"list": []any{"A"}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "ADD_REPLACE")) + }) + }) + + t.Run("set", func(t *testing.T) { + t.Run("changed non-empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{"A"}}, + Config2: map[string]any{"set": []any{"B"}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE_REPLACE")) + }) + + t.Run("changed to empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{"A"}}, + Config2: map[string]any{"set": []any{}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "DELETE_REPLACE")) + }) + + t.Run("changed from empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{}}, + Config2: map[string]any{"set": []any{"A"}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "ADD_REPLACE")) + }) + }) + + t.Run("map", func(t *testing.T) { + t.Run("changed non-empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"map": map[string]any{"A": "A"}}, + Config2: map[string]any{"map": map[string]any{"A": "B"}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE_REPLACE")) + }) + + t.Run("changed to empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"map": map[string]any{"A": "A"}}, + Config2: map[string]any{"map": map[string]any{}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "DELETE_REPLACE")) + }) + + t.Run("changed from empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"map": map[string]any{}}, + Config2: map[string]any{"map": map[string]any{"A": "A"}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "ADD_REPLACE")) + }) + }) +} + +func TestBlockCollectionForceNew(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "set": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "other": { + Type: schema.TypeString, + Optional: true, + }, + }, + } + + t.Run("list", func(t *testing.T) { + t.Run("changed non-empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"list": []any{map[string]any{"x": "B"}}}, + }) + + require.Equal(t, []string{"update"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE")) + require.False(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE_REPLACE")) + }) + + t.Run("changed to empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"list": []any{}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "DELETE_REPLACE")) + }) + + t.Run("changed from empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{}}, + Config2: map[string]any{"list": []any{map[string]any{"x": "A"}}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "ADD_REPLACE")) + }) + }) + + t.Run("set", func(t *testing.T) { + t.Run("changed non-empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"set": []any{map[string]any{"x": "B"}}}, + }) + + require.Equal(t, []string{"update"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE")) + require.False(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE_REPLACE")) + }) + + t.Run("changed to empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"set": []any{}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "DELETE_REPLACE")) + }) + + t.Run("changed from empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{}}, + Config2: map[string]any{"set": []any{map[string]any{"x": "A"}}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "ADD_REPLACE")) + }) + }) +} + +func TestBlockCollectionElementForceNew(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "list": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "set": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + }, + } + + t.Run("list", func(t *testing.T) { + t.Run("changed non-empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"list": []any{map[string]any{"x": "B"}}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE_REPLACE")) + }) + + t.Run("changed to empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"list": []any{}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "DELETE_REPLACE")) + }) + + t.Run("changed from empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"list": []any{}}, + Config2: map[string]any{"list": []any{map[string]any{"x": "A"}}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "ADD_REPLACE")) + }) + }) + + t.Run("set", func(t *testing.T) { + t.Run("changed non-empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"set": []any{map[string]any{"x": "B"}}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "UPDATE_REPLACE")) + }) + + t.Run("changed to empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{map[string]any{"x": "A"}}}, + Config2: map[string]any{"set": []any{}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "DELETE_REPLACE")) + }) + + t.Run("changed from empty", func(t *testing.T) { + res := runDiffCheck(t, diffTestCase{ + Resource: res, + Config1: map[string]any{"set": []any{}}, + Config2: map[string]any{"set": []any{map[string]any{"x": "A"}}}, + }) + + require.Equal(t, []string{"create", "delete"}, res.TFDiff.Actions) + require.True(t, findKindInPulumiDetailedDiff(res.PulumiDiff.DetailedDiff, "ADD_REPLACE")) + }) + }) } diff --git a/pkg/tests/schema_pulumi_test.go b/pkg/tests/schema_pulumi_test.go index 63265ab23..e71f76bca 100644 --- a/pkg/tests/schema_pulumi_test.go +++ b/pkg/tests/schema_pulumi_test.go @@ -1877,6 +1877,8 @@ resources: } func TestDetailedDiffPlainTypes(t *testing.T) { + // TODO: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") resMap := map[string]*schema.Resource{ "prov_test": { Schema: map[string]*schema.Schema{ @@ -1956,7 +1958,6 @@ resources: props1 interface{} props2 interface{} expected autogold.Value - skip bool }{ { "string unchanged", @@ -1968,7 +1969,6 @@ resources: Resources: 2 unchanged `), - false, }, { "string added", @@ -1985,7 +1985,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "string removed", @@ -2002,7 +2001,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "string changed", @@ -2019,7 +2017,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "list unchanged", @@ -2031,9 +2028,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "list added", map[string]interface{}{}, @@ -2047,16 +2042,12 @@ Resources: + listProps: [ + [0]: "val" ] - + listProps: [ - + [0]: "val" - ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2233]: Missing diff + // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior { "list added empty", map[string]interface{}{}, @@ -2067,9 +2058,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "list removed", map[string]interface{}{"listProps": []interface{}{"val"}}, @@ -2083,16 +2072,12 @@ Resources: - listProps: [ - [0]: "val" ] - - listProps: [ - - [0]: "val" - ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2233]: Missing diff + // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior { "list removed empty", map[string]interface{}{"listProps": []interface{}{}}, @@ -2103,7 +2088,6 @@ Resources: Resources: 2 unchanged `), - false, }, { "list element added front", @@ -2124,7 +2108,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "list element added back", @@ -2137,15 +2120,12 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listProps: [ - [0]: "val1" - [1]: "val2" + [2]: "val3" ] Resources: ~ 1 to update 1 unchanged `), - false, }, { "list element added middle", @@ -2158,7 +2138,6 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listProps: [ - [0]: "val1" ~ [1]: "val3" => "val2" + [2]: "val3" ] @@ -2166,7 +2145,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "list element removed front", @@ -2187,7 +2165,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "list element removed back", @@ -2200,15 +2177,12 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listProps: [ - [0]: "val1" - [1]: "val2" - [2]: "val3" ] Resources: ~ 1 to update 1 unchanged `), - false, }, { "list element removed middle", @@ -2221,7 +2195,6 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listProps: [ - [0]: "val1" ~ [1]: "val2" => "val3" - [2]: "val3" ] @@ -2229,7 +2202,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "list element changed", @@ -2248,7 +2220,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "set unchanged", @@ -2260,9 +2231,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "set added", map[string]interface{}{}, @@ -2276,16 +2245,12 @@ Resources: + setProps: [ + [0]: "val" ] - + setProps: [ - + [0]: "val" - ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2233]: Missing diff + // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior { "set added empty", map[string]interface{}{}, @@ -2296,9 +2261,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "set removed", map[string]interface{}{"setProps": []interface{}{"val"}}, @@ -2312,16 +2275,12 @@ Resources: - setProps: [ - [0]: "val" ] - - setProps: [ - - [0]: "val" - ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2233]: Missing diff + // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior { "set removed empty", map[string]interface{}{"setProps": []interface{}{}}, @@ -2332,30 +2291,26 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2235]: Wrong number of additions { "set element added front", map[string]interface{}{"setProps": []interface{}{"val2", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ setProps: [ - ~ [0]: "val2" => "val1" - ~ [1]: "val3" => "val2" - + [2]: "val3" - ] - Resources: - ~ 1 to update - 1 unchanged - `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ setProps: [ + ~ [0]: "val2" => "val1" + ~ [1]: "val3" => "val2" + + [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), }, { "set element added back", @@ -2368,61 +2323,51 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - [0]: "val1" - [1]: "val2" + [2]: "val3" ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2235]: Wrong number of additions { "set element added middle", map[string]interface{}{"setProps": []interface{}{"val1", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ setProps: [ - [0]: "val1" - + [1]: "val2" - + [2]: "val3" - ] - Resources: - ~ 1 to update - 1 unchanged - - `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ setProps: [ + ~ [1]: "val3" => "val2" + + [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), }, { "set element removed front", map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val2", "val3"}}, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ setProps: [ - ~ [0]: "val1" => "val2" - ~ [1]: "val2" => "val3" - - [2]: "val3" - ] - Resources: - ~ 1 to update - 1 unchanged + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ setProps: [ + ~ [0]: "val1" => "val2" + ~ [1]: "val2" => "val3" + - [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, }, { "set element removed back", @@ -2435,38 +2380,31 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - [0]: "val1" - [1]: "val2" - [2]: "val3" ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2235]: Wrong number of removals { "set element removed middle", map[string]interface{}{"setProps": []interface{}{"val1", "val2", "val3"}}, map[string]interface{}{"setProps": []interface{}{"val1", "val3"}}, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ setProps: [ - [0]: "val1" - ~ [1]: "val2" => "val3" - - [2]: "val3" - ] - Resources: - ~ 1 to update - 1 unchanged - `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ setProps: [ + ~ [1]: "val2" => "val3" + - [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), }, { "set element changed", @@ -2485,7 +2423,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "map unchanged", @@ -2497,9 +2434,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "map added", map[string]interface{}{}, @@ -2513,16 +2448,12 @@ Resources: + mapProp: { + key: "val" } - + mapProp: { - + key: "val" - } Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2233]: Missing diff + // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior { "map added empty", map[string]interface{}{}, @@ -2533,9 +2464,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "map removed", map[string]interface{}{"mapProp": map[string]interface{}{"key": "val"}}, @@ -2549,16 +2478,12 @@ Resources: - mapProp: { - key: "val" } - - mapProp: { - - key: "val" - } Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2233]: Missing diff + // pulumi/pulumi-terraform-bridge#2233: This is the intended behavior { "map removed empty", map[string]interface{}{"mapProp": map[string]interface{}{}}, @@ -2569,9 +2494,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "map element added", map[string]interface{}{"mapProp": map[string]interface{}{}}, @@ -2585,14 +2508,10 @@ Resources: + mapProp: { + key: "val" } - + mapProp: { - + key: "val" - } Resources: ~ 1 to update 1 unchanged `), - false, }, { "map element removed", @@ -2611,7 +2530,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "map value changed", @@ -2630,7 +2548,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "map key changed", @@ -2650,7 +2567,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "list block unchanged", @@ -2662,7 +2578,6 @@ Resources: Resources: 2 unchanged `), - false, }, { "list block added", @@ -2674,16 +2589,15 @@ Resources: ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ listBlocks: [ - + [0]: { - + prop : "val" - } + + listBlocks: [ + + [0]: { + + prop : "val" + } ] Resources: ~ 1 to update 1 unchanged `), - false, }, // This is expected to be a no-op because blocks can not be nil in TF { @@ -2696,7 +2610,6 @@ Resources: Resources: 2 unchanged `), - false, }, { "list block added empty object", @@ -2708,17 +2621,15 @@ Resources: ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ listBlocks: [ - + [0]: { - } + + listBlocks: [ + + [0]: { + } ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "list block removed", map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, @@ -2734,16 +2645,10 @@ Resources: - prop: "val" } ] - - listBlocks: [ - - [0]: { - - prop: "val" - } - ] Resources: ~ 1 to update 1 unchanged `), - false, }, // This is expected to be a no-op because blocks can not be nil in TF { @@ -2756,9 +2661,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2399] nested prop diff { "list block removed empty object", map[string]interface{}{"listBlocks": []interface{}{map[string]interface{}{}}}, @@ -2778,9 +2681,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "list block element added front", map[string]interface{}{"listBlocks": []interface{}{ @@ -2800,12 +2701,10 @@ Resources: [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listBlocks: [ ~ [0]: { - + __defaults: [] - ~ prop : "val2" => "val1" + ~ prop: "val2" => "val1" } ~ [1]: { - + __defaults: [] - ~ prop : "val3" => "val2" + ~ prop: "val3" => "val2" } + [2]: { + prop : "val3" @@ -2815,9 +2714,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "list block element added back", map[string]interface{}{"listBlocks": []interface{}{ @@ -2836,14 +2733,6 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } - ~ [1]: { - + __defaults: [] - prop : "val2" - } + [2]: { + prop : "val3" } @@ -2852,9 +2741,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "list block element added middle", map[string]interface{}{"listBlocks": []interface{}{ @@ -2873,13 +2760,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } ~ [1]: { - + __defaults: [] - ~ prop : "val3" => "val2" + ~ prop: "val3" => "val2" } + [2]: { + prop : "val3" @@ -2889,9 +2771,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "list block element removed front", map[string]interface{}{"listBlocks": []interface{}{ @@ -2911,12 +2791,10 @@ Resources: [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listBlocks: [ ~ [0]: { - + __defaults: [] - ~ prop : "val1" => "val2" + ~ prop: "val1" => "val2" } ~ [1]: { - + __defaults: [] - ~ prop : "val2" => "val3" + ~ prop: "val2" => "val3" } - [2]: { - prop: "val3" @@ -2926,9 +2804,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "list block element removed back", map[string]interface{}{"listBlocks": []interface{}{ @@ -2947,14 +2823,6 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } - ~ [1]: { - + __defaults: [] - prop : "val2" - } - [2]: { - prop: "val3" } @@ -2963,9 +2831,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "list block element removed middle", map[string]interface{}{"listBlocks": []interface{}{ @@ -2984,13 +2850,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ listBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } ~ [1]: { - + __defaults: [] - ~ prop : "val2" => "val3" + ~ prop: "val2" => "val3" } - [2]: { - prop: "val3" @@ -3000,7 +2861,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "list block element changed", @@ -3025,7 +2885,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "set block unchanged", @@ -3037,7 +2896,6 @@ Resources: Resources: 2 unchanged `), - false, }, { "set block added", @@ -3049,16 +2907,15 @@ Resources: ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ setBlocks: [ - + [0]: { - + prop : "val" - } + + setBlocks: [ + + [0]: { + + prop : "val" + } ] Resources: ~ 1 to update 1 unchanged `), - false, }, // This is expected to be a no-op because blocks can not be nil in TF { @@ -3071,7 +2928,6 @@ Resources: Resources: 2 unchanged `), - false, }, { "set block added empty object", @@ -3083,17 +2939,15 @@ Resources: ~ prov:index/test:Test: (update) [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - ~ setBlocks: [ - + [0]: { - } + + setBlocks: [ + + [0]: { + } ] Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "set block removed", map[string]interface{}{"setBlocks": []interface{}{map[string]interface{}{"prop": "val"}}}, @@ -3109,16 +2963,10 @@ Resources: - prop: "val" } ] - - setBlocks: [ - - [0]: { - - prop: "val" - } - ] Resources: ~ 1 to update 1 unchanged `), - false, }, // This is expected to be a no-op because blocks can not be nil in TF { @@ -3131,7 +2979,6 @@ Resources: Resources: 2 unchanged `), - false, }, // TODO[pulumi/pulumi-terraform-bridge#2399] nested prop diff { @@ -3153,9 +3000,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "set block element added front", map[string]interface{}{"setBlocks": []interface{}{ @@ -3175,12 +3020,10 @@ Resources: [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ ~ [0]: { - + __defaults: [] - ~ prop : "val2" => "val1" + ~ prop: "val2" => "val1" } ~ [1]: { - + __defaults: [] - ~ prop : "val3" => "val2" + ~ prop: "val3" => "val2" } + [2]: { + prop : "val3" @@ -3190,10 +3033,7 @@ Resources: ~ 1 to update 1 unchanged `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "set block element added back", map[string]interface{}{"setBlocks": []interface{}{ @@ -3212,14 +3052,6 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } - ~ [1]: { - + __defaults: [] - prop : "val2" - } + [2]: { + prop : "val3" } @@ -3228,10 +3060,7 @@ Resources: ~ 1 to update 1 unchanged `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "set block element added middle", map[string]interface{}{"setBlocks": []interface{}{ @@ -3250,13 +3079,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } ~ [1]: { - + __defaults: [] - ~ prop : "val3" => "val2" + ~ prop: "val3" => "val2" } + [2]: { + prop : "val3" @@ -3266,11 +3090,7 @@ Resources: ~ 1 to update 1 unchanged `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "set block element removed front", map[string]interface{}{"setBlocks": []interface{}{ @@ -3290,12 +3110,10 @@ Resources: [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ ~ [0]: { - + __defaults: [] - ~ prop : "val1" => "val2" + ~ prop: "val1" => "val2" } ~ [1]: { - + __defaults: [] - ~ prop : "val2" => "val3" + ~ prop: "val2" => "val3" } - [2]: { - prop: "val3" @@ -3305,10 +3123,7 @@ Resources: ~ 1 to update 1 unchanged `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "set block element removed back", map[string]interface{}{"setBlocks": []interface{}{ @@ -3327,14 +3142,6 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } - ~ [1]: { - + __defaults: [] - prop : "val2" - } - [2]: { - prop: "val3" } @@ -3343,10 +3150,7 @@ Resources: ~ 1 to update 1 unchanged `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, }, - // TODO[pulumi/pulumi-terraform-bridge#2400] __defaults appearing in the diff { "set block element removed middle", map[string]interface{}{"setBlocks": []interface{}{ @@ -3365,13 +3169,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - + __defaults: [] - prop : "val1" - } ~ [1]: { - + __defaults: [] - ~ prop : "val2" => "val3" + ~ prop: "val2" => "val3" } - [2]: { - prop: "val3" @@ -3381,8 +3180,6 @@ Resources: ~ 1 to update 1 unchanged `), - // TODO[pulumi/pulumi-terraform-bridge#2325]: Non-deterministic output - true, }, { "set block element changed", @@ -3407,7 +3204,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "maxItemsOne block unchanged", @@ -3419,9 +3215,7 @@ Resources: Resources: 2 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "maxItemsOne block added", map[string]interface{}{}, @@ -3435,14 +3229,10 @@ Resources: + maxItemsOneBlock: { + prop : "val" } - + maxItemsOneBlock: { - + prop : "val" - } Resources: ~ 1 to update 1 unchanged `), - false, }, { "maxItemsOne block added empty", @@ -3460,9 +3250,7 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, - // TODO[pulumi/pulumi-terraform-bridge#2234]: Duplicated diff { "maxItemsOne block removed", map[string]interface{}{"maxItemsOneBlock": map[string]interface{}{"prop": "val"}}, @@ -3476,14 +3264,10 @@ Resources: - maxItemsOneBlock: { - prop: "val" } - - maxItemsOneBlock: { - - prop: "val" - } Resources: ~ 1 to update 1 unchanged `), - false, }, // TODO[pulumi/pulumi-terraform-bridge#2399] nested prop diff { @@ -3503,7 +3287,6 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, { "maxItemsOne block changed", @@ -3522,14 +3305,10 @@ Resources: ~ 1 to update 1 unchanged `), - false, }, } { tc := tc t.Run(tc.name, func(t *testing.T) { - if tc.skip { - t.Skip("skipping known failing test") - } t.Parallel() props1, err := json.Marshal(tc.props1) require.NoError(t, err) @@ -4280,8 +4059,8 @@ outputs: } t.Run("PRC enabled", func(t *testing.T) { - // TODO[pulumi/pulumi-terraform-bridge#2427]: Incorrect detailed diff with unknown elements - t.Skip("Skipping until pulumi/pulumi-terraform-bridge#2427 is resolved") + // TODO: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") runTest(t, true, autogold.Expect(`Previewing update (test): pulumi:pulumi:Stack: (same) [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] @@ -4323,3 +4102,60 @@ Resources: `)) }) } + +func TestMakeTerraformResultNilVsEmptyMap(t *testing.T) { + // Nil and empty maps are not equal + nilMap := resource.NewObjectProperty(nil) + emptyMap := resource.NewObjectProperty(resource.PropertyMap{}) + + assert.True(t, nilMap.DeepEquals(emptyMap)) + assert.NotEqual(t, emptyMap.ObjectValue(), nilMap.ObjectValue()) + + // Check that MakeTerraformResult maintains that difference + const resName = "prov_test" + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + } + + prov := &schema.Provider{ + ResourcesMap: resMap, + } + bridgedProvider := pulcheck.BridgedProvider(t, "prov", prov) + + ctx := context.Background() + shimProv := bridgedProvider.P + + res := shimProv.ResourcesMap().Get(resName) + + t.Run("NilMap", func(t *testing.T) { + // Create a resource with a nil map + state, err := res.InstanceState("0", map[string]interface{}{}, map[string]interface{}{}) + assert.NoError(t, err) + + props, err := tfbridge.MakeTerraformResult(ctx, shimProv, state, res.Schema(), nil, nil, true) + assert.NoError(t, err) + assert.NotNil(t, props) + assert.True(t, props["test"].V == nil) + }) + + t.Run("EmptyMap", func(t *testing.T) { + // Create a resource with an empty map + state, err := res.InstanceState("0", map[string]interface{}{"test": map[string]interface{}{}}, map[string]interface{}{}) + assert.NoError(t, err) + + props, err := tfbridge.MakeTerraformResult(ctx, shimProv, state, res.Schema(), nil, nil, true) + assert.NoError(t, err) + assert.NotNil(t, props) + assert.True(t, props["test"].DeepEquals(emptyMap)) + }) +} diff --git a/pkg/tfbridge/detailed_diff.go b/pkg/tfbridge/detailed_diff.go new file mode 100644 index 000000000..f7d6a00a0 --- /dev/null +++ b/pkg/tfbridge/detailed_diff.go @@ -0,0 +1,419 @@ +package tfbridge + +import ( + "cmp" + "context" + "slices" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/walk" +) + +func isPresent(val resource.PropertyValue) bool { + return !val.IsNull() && + !(val.IsArray() && val.ArrayValue() == nil) && + !(val.IsObject() && val.ObjectValue() == nil) +} + +func isForceNew(tfs shim.Schema, ps *SchemaInfo) bool { + return (tfs != nil && tfs.ForceNew()) || + (ps != nil && ps.ForceNew != nil && *ps.ForceNew) +} + +func sortedMergedKeys[K cmp.Ordered, V any, M ~map[K]V](a, b M) []K { + keys := make(map[K]struct{}) + for k := range a { + keys[k] = struct{}{} + } + for k := range b { + keys[k] = struct{}{} + } + keysSlice := make([]K, 0, len(keys)) + for k := range keys { + keysSlice = append(keysSlice, k) + } + slices.Sort(keysSlice) + return keysSlice +} + +func promoteToReplace(diff *pulumirpc.PropertyDiff) *pulumirpc.PropertyDiff { + if diff == nil { + return nil + } + + kind := diff.GetKind() + switch kind { + case pulumirpc.PropertyDiff_ADD: + return &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD_REPLACE} + case pulumirpc.PropertyDiff_DELETE: + return &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE_REPLACE} + case pulumirpc.PropertyDiff_UPDATE: + return &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE} + default: + return diff + } +} + +type baseDiff string + +const ( + undecidedDiff baseDiff = "" + noDiff baseDiff = "NoDiff" + addDiff baseDiff = "Add" + deleteDiff baseDiff = "Delete" + updateDiff baseDiff = "Update" +) + +func (b baseDiff) ToPropertyDiff() *pulumirpc.PropertyDiff { + switch b { + case noDiff: + return nil + case addDiff: + return &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD} + case deleteDiff: + return &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE} + case updateDiff: + return &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + case undecidedDiff: + contract.Failf("diff should not be undecided") + default: + contract.Failf("unexpected base diff %s", b) + } + contract.Failf("unreachable") + return nil +} + +func makeBaseDiff(old, new resource.PropertyValue) baseDiff { + oldPresent := isPresent(old) + newPresent := isPresent(new) + if !oldPresent { + if !newPresent { + return noDiff + } + + return addDiff + } + if !newPresent { + return deleteDiff + } + + if new.IsComputed() { + return updateDiff + } + + return undecidedDiff +} + +type ( + detailedDiffKey string + propertyPath resource.PropertyPath +) + +func newPropertyPath(root resource.PropertyKey) propertyPath { + return propertyPath{string(root)} +} + +func (k propertyPath) String() string { + return resource.PropertyPath(k).String() +} + +func (k propertyPath) Key() detailedDiffKey { + return detailedDiffKey(k.String()) +} + +func (k propertyPath) append(subkey interface{}) propertyPath { + return append(k, subkey) +} + +func (k propertyPath) Subpath(subkey string) propertyPath { + return k.append(subkey) +} + +func (k propertyPath) Index(i int) propertyPath { + return k.append(i) +} + +func (k propertyPath) IsReservedKey() bool { + leaf := k[len(k)-1] + return leaf == "__meta" || leaf == "__defaults" +} + +func mapHasReplacements(m map[detailedDiffKey]*pulumirpc.PropertyDiff) bool { + for _, diff := range m { + if diff.GetKind() == pulumirpc.PropertyDiff_ADD_REPLACE || + diff.GetKind() == pulumirpc.PropertyDiff_DELETE_REPLACE || + diff.GetKind() == pulumirpc.PropertyDiff_UPDATE_REPLACE { + return true + } + } + return false +} + +type detailedDiffer struct { + tfs shim.SchemaMap + ps map[string]*SchemaInfo +} + +func (differ detailedDiffer) propertyPathToSchemaPath(path propertyPath) walk.SchemaPath { + return PropertyPathToSchemaPath(resource.PropertyPath(path), differ.tfs, differ.ps) +} + +// getEffectiveType returns the pulumi-visible type of the property at the given path. +// It takes into account any MaxItemsOne flattening which might have occurred. +// Specifically: +// - If the property is a list/set with MaxItemsOne, it returns the type of the element. +// - Otherwise it returns the type of the property. +func (differ detailedDiffer) getEffectiveType(path walk.SchemaPath) shim.ValueType { + tfs, ps, err := LookupSchemas(path, differ.tfs, differ.ps) + + if err != nil || tfs == nil { + return shim.TypeInvalid + } + + if IsMaxItemsOne(tfs, ps) { + return differ.getEffectiveType(path.Element()) + } + + return tfs.Type() +} + +func (differ detailedDiffer) lookupSchemas(path propertyPath) (shim.Schema, *info.Schema, error) { + schemaPath := PropertyPathToSchemaPath(resource.PropertyPath(path), differ.tfs, differ.ps) + return LookupSchemas(schemaPath, differ.tfs, differ.ps) +} + +func (differ detailedDiffer) isForceNew(pair propertyPath) bool { + // A change on a property might trigger a replacement if: + // - The property itself is marked as ForceNew + // - The direct parent property is a collection (list, set, map) and is marked as ForceNew + // See pkg/cross-tests/diff_cross_test.go + // TestAttributeCollectionForceNew, TestBlockCollectionForceNew, TestBlockCollectionElementForceNew + // for a full case study of replacements in TF + tfs, ps, err := differ.lookupSchemas(pair) + if err != nil { + return false + } + if isForceNew(tfs, ps) { + return true + } + + if len(pair) == 1 { + return false + } + + parent := pair[:len(pair)-1] + tfs, ps, err = differ.lookupSchemas(parent) + if err != nil { + return false + } + // Note this is mimicking the TF behaviour, so the effective type is not considered here. + if tfs.Type() != shim.TypeList && tfs.Type() != shim.TypeSet && tfs.Type() != shim.TypeMap { + return false + } + return isForceNew(tfs, ps) +} + +// We do not short-circuit detailed diffs when comparing non-nil properties against nil ones. The reason for that is +// that a replace might be triggered by a ForceNew inside a nested property of a non-ForceNew property. We instead +// always walk the full tree even when comparing against a nil property. We then later do a simplification step for +// the detailed diff in simplifyDiff in order to reduce the diff to what the user expects to see. +// See [pulumi/pulumi-terraform-bridge#2405] for more details. +func (differ detailedDiffer) simplifyDiff( + diff map[detailedDiffKey]*pulumirpc.PropertyDiff, path propertyPath, old, new resource.PropertyValue, +) (map[detailedDiffKey]*pulumirpc.PropertyDiff, bool) { + baseDiff := makeBaseDiff(old, new) + if baseDiff == undecidedDiff { + return nil, false + } + propDiff := baseDiff.ToPropertyDiff() + if propDiff == nil { + return nil, true + } + if differ.isForceNew(path) || mapHasReplacements(diff) { + propDiff = promoteToReplace(propDiff) + } + return map[detailedDiffKey]*pulumirpc.PropertyDiff{path.Key(): propDiff}, true +} + +// makePlainPropDiff is used for plain properties and ones with an unknown schema. +// It does not access the TF schema, so it does not know about the type of the property. +func (differ detailedDiffer) makePlainPropDiff( + path propertyPath, old, new resource.PropertyValue, +) map[detailedDiffKey]*pulumirpc.PropertyDiff { + baseDiff := makeBaseDiff(old, new) + isForceNew := differ.isForceNew(path) + var propDiff *pulumirpc.PropertyDiff + if baseDiff != undecidedDiff { + propDiff = baseDiff.ToPropertyDiff() + } else if !old.DeepEquals(new) { + propDiff = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + } + + if isForceNew { + propDiff = promoteToReplace(propDiff) + } + + if propDiff != nil { + return map[detailedDiffKey]*pulumirpc.PropertyDiff{path.Key(): propDiff} + } + return nil +} + +func (differ detailedDiffer) makePropDiff( + path propertyPath, old, new resource.PropertyValue, +) map[detailedDiffKey]*pulumirpc.PropertyDiff { + if path.IsReservedKey() { + return nil + } + propType := differ.getEffectiveType(differ.propertyPathToSchemaPath(path)) + + switch propType { + case shim.TypeList: + return differ.makeListDiff(path, old, new) + case shim.TypeSet: + // TODO[pulumi/pulumi-terraform-bridge#2200]: Implement set diffing + return differ.makeListDiff(path, old, new) + case shim.TypeMap: + // Note that TF objects are represented as maps when returned by LookupSchemas + return differ.makeMapDiff(path, old, new) + default: + return differ.makePlainPropDiff(path, old, new) + } +} + +func (differ detailedDiffer) makeListDiff( + path propertyPath, old, new resource.PropertyValue, +) map[detailedDiffKey]*pulumirpc.PropertyDiff { + diff := make(map[detailedDiffKey]*pulumirpc.PropertyDiff) + oldList := []resource.PropertyValue{} + newList := []resource.PropertyValue{} + if isPresent(old) && old.IsArray() { + oldList = old.ArrayValue() + } + if isPresent(new) && new.IsArray() { + newList = new.ArrayValue() + } + + // naive diffing of lists + // TODO[pulumi/pulumi-terraform-bridge#2295]: implement a more sophisticated diffing algorithm + // investigate how this interacts with force new - is identity preserved or just order + longerLen := max(len(oldList), len(newList)) + for i := 0; i < longerLen; i++ { + elem := func(l []resource.PropertyValue) resource.PropertyValue { + if i < len(l) { + return l[i] + } + return resource.NewNullProperty() + } + elemKey := path.Index(i) + + d := differ.makePropDiff(elemKey, elem(oldList), elem(newList)) + for subKey, subDiff := range d { + diff[subKey] = subDiff + } + } + + simplerDiff, isSimplified := differ.simplifyDiff(diff, path, old, new) + if isSimplified { + return simplerDiff + } + + return diff +} + +func (differ detailedDiffer) makeMapDiff( + path propertyPath, old, new resource.PropertyValue, +) map[detailedDiffKey]*pulumirpc.PropertyDiff { + diff := make(map[detailedDiffKey]*pulumirpc.PropertyDiff) + oldMap := resource.PropertyMap{} + newMap := resource.PropertyMap{} + if isPresent(old) && old.IsObject() { + oldMap = old.ObjectValue() + } + if isPresent(new) && new.IsObject() { + newMap = new.ObjectValue() + } + + for _, k := range sortedMergedKeys(oldMap, newMap) { + subindex := path.Subpath(string(k)) + oldVal := oldMap[k] + newVal := newMap[k] + + elemDiff := differ.makePropDiff(subindex, oldVal, newVal) + + for subKey, subDiff := range elemDiff { + diff[subKey] = subDiff + } + } + + simplerDiff, isSimplified := differ.simplifyDiff(diff, path, old, new) + if isSimplified { + return simplerDiff + } + + return diff +} + +func (differ detailedDiffer) makeDetailedDiffPropertyMap( + oldState, plannedState resource.PropertyMap, +) map[string]*pulumirpc.PropertyDiff { + diff := make(map[detailedDiffKey]*pulumirpc.PropertyDiff) + for _, k := range sortedMergedKeys(oldState, plannedState) { + old := oldState[k] + new := plannedState[k] + + path := newPropertyPath(k) + propDiff := differ.makePropDiff(path, old, new) + + for subKey, subDiff := range propDiff { + diff[subKey] = subDiff + } + } + + result := make(map[string]*pulumirpc.PropertyDiff) + for k, v := range diff { + result[string(k)] = v + } + + return result +} + +func makeDetailedDiffV2( + ctx context.Context, + tfs shim.SchemaMap, + ps map[string]*SchemaInfo, + res shim.Resource, + prov shim.Provider, + state shim.InstanceState, + diff shim.InstanceDiff, + assets AssetTable, + supportsSecrets bool, +) (map[string]*pulumirpc.PropertyDiff, error) { + // We need to compare the new and olds after all transformations have been applied. + // ex. state upgrades, implementation-specific normalizations etc. + proposedState, err := diff.ProposedState(res, state) + if err != nil { + return nil, err + } + props, err := MakeTerraformResult(ctx, prov, proposedState, tfs, ps, assets, supportsSecrets) + if err != nil { + return nil, err + } + + prior, err := diff.PriorState() + if err != nil { + return nil, err + } + priorProps, err := MakeTerraformResult(ctx, prov, prior, tfs, ps, assets, supportsSecrets) + if err != nil { + return nil, err + } + + differ := detailedDiffer{tfs: tfs, ps: ps} + return differ.makeDetailedDiffPropertyMap(priorProps, props), nil +} diff --git a/pkg/tfbridge/detailed_diff_test.go b/pkg/tfbridge/detailed_diff_test.go new file mode 100644 index 000000000..e75216d97 --- /dev/null +++ b/pkg/tfbridge/detailed_diff_test.go @@ -0,0 +1,1755 @@ +package tfbridge + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + "github.com/stretchr/testify/require" + "gotest.tools/v3/assert" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + shimschema "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema" + shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" +) + +func TestDiffPair(t *testing.T) { + assert.Equal(t, (newPropertyPath("foo").Subpath("bar")).Key(), detailedDiffKey("foo.bar")) + assert.Equal(t, newPropertyPath("foo").Subpath("bar").Subpath("baz").Key(), detailedDiffKey("foo.bar.baz")) + assert.Equal(t, newPropertyPath("foo").Subpath("bar.baz").Key(), detailedDiffKey(`foo["bar.baz"]`)) + assert.Equal(t, newPropertyPath("foo").Index(2).Key(), detailedDiffKey("foo[2]")) +} + +func TestReservedKey(t *testing.T) { + assert.Equal(t, newPropertyPath("foo").Subpath("__meta").IsReservedKey(), true) + assert.Equal(t, newPropertyPath("foo").Subpath("__defaults").IsReservedKey(), true) + assert.Equal(t, newPropertyPath("__defaults").IsReservedKey(), true) + assert.Equal(t, newPropertyPath("foo").Subpath("bar").IsReservedKey(), false) +} + +func TestSchemaLookupMaxItemsOnePlain(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "string_prop": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } + + differ := detailedDiffer{ + tfs: shimv2.NewSchemaMap(sdkv2Schema), + } + + sch, _, err := differ.lookupSchemas(newPropertyPath("string_prop")) + require.NoError(t, err) + require.NotNil(t, sch) + require.Equal(t, sch.Type(), shim.TypeList) +} + +func TestSchemaLookupMaxItemsOne(t *testing.T) { + res := schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bar": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + } + + differ := detailedDiffer{ + tfs: shimv2.NewSchemaMap(res.Schema), + } + + sch, _, err := differ.lookupSchemas(newPropertyPath("foo")) + require.NoError(t, err) + require.NotNil(t, sch) + require.Equal(t, sch.Type(), shim.TypeList) + + sch, _, err = differ.lookupSchemas(newPropertyPath("foo").Subpath("bar")) + require.NoError(t, err) + require.NotNil(t, sch) + require.Equal(t, sch.Type(), shim.TypeString) +} + +func TestSchemaLookupMap(t *testing.T) { + res := schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } + + differ := detailedDiffer{ + tfs: shimv2.NewSchemaMap(res.Schema), + } + + sch, _, err := differ.lookupSchemas(newPropertyPath("foo")) + require.NoError(t, err) + require.NotNil(t, sch) + require.Equal(t, sch.Type(), shim.TypeMap) + + sch, _, err = differ.lookupSchemas(propertyPath{"foo", "bar"}) + require.NoError(t, err) + require.NotNil(t, sch) + require.Equal(t, sch.Type(), shim.TypeString) +} + +func TestMakeBaseDiff(t *testing.T) { + nilVal := resource.NewNullProperty() + nilArr := resource.NewArrayProperty(nil) + nilMap := resource.NewObjectProperty(nil) + nonNilVal := resource.NewStringProperty("foo") + nonNilVal2 := resource.NewStringProperty("bar") + + assert.Equal(t, makeBaseDiff(nilVal, nilVal), noDiff) + assert.Equal(t, makeBaseDiff(nilVal, nilVal), noDiff) + assert.Equal(t, makeBaseDiff(nilVal, nonNilVal), addDiff) + assert.Equal(t, makeBaseDiff(nilVal, nonNilVal), addDiff) + assert.Equal(t, makeBaseDiff(nonNilVal, nilVal), deleteDiff) + assert.Equal(t, makeBaseDiff(nonNilVal, nilVal), deleteDiff) + assert.Equal(t, makeBaseDiff(nonNilVal, nilArr), deleteDiff) + assert.Equal(t, makeBaseDiff(nonNilVal, nilMap), deleteDiff) + assert.Equal(t, makeBaseDiff(nonNilVal, nonNilVal2), undecidedDiff) +} + +func TestMakePropDiff(t *testing.T) { + tests := []struct { + name string + old resource.PropertyValue + new resource.PropertyValue + etf shimschema.Schema + eps *SchemaInfo + expected *pulumirpc.PropertyDiff + }{ + { + name: "unchanged non-nil", + old: resource.NewStringProperty("same"), + new: resource.NewStringProperty("same"), + expected: nil, + }, + { + name: "unchanged nil", + old: resource.NewNullProperty(), + new: resource.NewNullProperty(), + expected: nil, + }, + { + name: "unchanged not present", + old: resource.NewNullProperty(), + new: resource.NewNullProperty(), + expected: nil, + }, + { + name: "added()", + old: resource.NewNullProperty(), + new: resource.NewStringProperty("new"), + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD}, + }, + { + name: "deleted()", + old: resource.NewStringProperty("old"), + new: resource.NewNullProperty(), + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE}, + }, + { + name: "changed non-nil", + old: resource.NewStringProperty("old"), + new: resource.NewStringProperty("new"), + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + { + name: "changed from nil", + old: resource.NewNullProperty(), + new: resource.NewStringProperty("new"), + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD}, + }, + { + name: "changed to nil", + old: resource.NewStringProperty("old"), + new: resource.NewNullProperty(), + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE}, + }, + { + name: "tf force new unchanged", + old: resource.NewStringProperty("old"), + new: resource.NewStringProperty("old"), + etf: shimschema.Schema{ForceNew: true}, + expected: nil, + }, + { + name: "tf force new changed non-nil", + old: resource.NewStringProperty("old"), + new: resource.NewStringProperty("new"), + etf: shimschema.Schema{ForceNew: true}, + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }, + { + name: "tf force new changed from nil", + old: resource.NewNullProperty(), + new: resource.NewStringProperty("new"), + etf: shimschema.Schema{ForceNew: true}, + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }, + { + name: "tf force new changed to nil", + old: resource.NewStringProperty("old"), + new: resource.NewNullProperty(), + etf: shimschema.Schema{ForceNew: true}, + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }, + { + name: "ps force new unchanged", + old: resource.NewStringProperty("old"), + new: resource.NewStringProperty("old"), + eps: &SchemaInfo{ForceNew: True()}, + expected: nil, + }, + { + name: "ps force new changed non-nil", + old: resource.NewStringProperty("old"), + new: resource.NewStringProperty("new"), + eps: &SchemaInfo{ForceNew: True()}, + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }, + { + name: "ps force new changed from nil", + old: resource.NewNullProperty(), + new: resource.NewStringProperty("new"), + eps: &SchemaInfo{ForceNew: True()}, + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }, + { + name: "ps force new changed to nil", + old: resource.NewStringProperty("old"), + new: resource.NewNullProperty(), + eps: &SchemaInfo{ForceNew: True()}, + expected: &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := detailedDiffer{ + tfs: shimschema.SchemaMap{"foo": tt.etf.Shim()}, + ps: map[string]*SchemaInfo{"foo": tt.eps}, + }.makePlainPropDiff(newPropertyPath("foo"), tt.old, tt.new) + + var expected map[detailedDiffKey]*pulumirpc.PropertyDiff + if tt.expected != nil { + expected = make(map[detailedDiffKey]*pulumirpc.PropertyDiff) + expected["foo"] = tt.expected + } + + require.Equal(t, expected, actual) + }) + } +} + +func added() map[string]*pulumirpc.PropertyDiff { + return map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD}, + } +} + +func updated() map[string]*pulumirpc.PropertyDiff { + return map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE}, + } +} + +func deleted() map[string]*pulumirpc.PropertyDiff { + return map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_DELETE}, + } +} + +var ComputedVal = resource.NewComputedProperty(resource.Computed{Element: resource.NewStringProperty("")}) + +func runDetailedDiffTest( + t *testing.T, + old, new resource.PropertyMap, + tfs shim.SchemaMap, + ps map[string]*SchemaInfo, + expected map[string]*pulumirpc.PropertyDiff, +) { + t.Helper() + differ := detailedDiffer{tfs: tfs, ps: ps} + actual := differ.makeDetailedDiffPropertyMap(old, new) + + require.Equal(t, expected, actual) +} + +func TestBasicDetailedDiff(t *testing.T) { + for _, tt := range []struct { + name string + emptyValue interface{} + value1 interface{} + value2 interface{} + tfs schema.Schema + listLike bool + objectLike bool + }{ + { + name: "string", + emptyValue: "", + value1: "foo", + value2: "bar", + tfs: schema.Schema{Type: schema.TypeString}, + }, + { + name: "int", + emptyValue: nil, + value1: 42, + value2: 43, + tfs: schema.Schema{Type: schema.TypeInt}, + }, + { + name: "bool", + emptyValue: nil, + value1: true, + value2: false, + tfs: schema.Schema{Type: schema.TypeBool}, + }, + { + name: "float", + emptyValue: nil, + value1: 42.0, + value2: 43.0, + tfs: schema.Schema{Type: schema.TypeFloat}, + }, + { + name: "list", + tfs: schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + emptyValue: []interface{}{}, + value1: []interface{}{"foo"}, + value2: []interface{}{"bar"}, + listLike: true, + }, + { + name: "map", + tfs: schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + emptyValue: map[string]interface{}{}, + value1: map[string]interface{}{"foo": "bar"}, + value2: map[string]interface{}{"foo": "baz"}, + objectLike: true, + }, + { + name: "set", + tfs: schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + emptyValue: []interface{}{}, + value1: []interface{}{"foo"}, + value2: []interface{}{"bar"}, + listLike: true, + }, + { + name: "list block", + tfs: schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + emptyValue: []interface{}{}, + value1: []interface{}{map[string]interface{}{"foo": "bar"}}, + value2: []interface{}{map[string]interface{}{"foo": "baz"}}, + listLike: true, + objectLike: true, + }, + { + name: "max items one list block", + tfs: schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + MaxItems: 1, + }, + emptyValue: map[string]interface{}{}, + value1: map[string]interface{}{"foo": "bar"}, + value2: map[string]interface{}{"foo": "baz"}, + objectLike: true, + }, + { + name: "set block", + tfs: schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + emptyValue: []interface{}{}, + value1: []interface{}{map[string]interface{}{"foo": "bar"}}, + value2: []interface{}{map[string]interface{}{"foo": "baz"}}, + listLike: true, + objectLike: true, + }, + { + name: "max items one set block", + tfs: schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + MaxItems: 1, + }, + emptyValue: map[string]interface{}{}, + value1: map[string]interface{}{"foo": "bar"}, + value2: map[string]interface{}{"foo": "baz"}, + objectLike: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": &tt.tfs, + } + + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + propertyMapNil := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": tt.emptyValue, + }, + ) + propertyMapValue1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": tt.value1, + }, + ) + propertyMapValue2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": tt.value2, + }, + ) + propertyMapComputed := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": ComputedVal, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapValue1, propertyMapValue1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + expected := make(map[string]*pulumirpc.PropertyDiff) + if tt.listLike && tt.objectLike { + expected["foo[0].foo"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + } else if tt.listLike { + expected["foo[0]"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + } else if tt.objectLike { + expected["foo.foo"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + } else { + expected["foo"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + } + runDetailedDiffTest(t, propertyMapValue1, propertyMapValue2, tfs, ps, expected) + }) + + t.Run("changed non-empty computed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapValue1, propertyMapComputed, tfs, ps, updated()) + }) + + t.Run("added", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapNil, propertyMapValue1, tfs, ps, added()) + }) + + if tt.emptyValue != nil { + t.Run("added empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapNil, propertyMapEmpty, tfs, ps, added()) + }) + } + + t.Run("added computed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapNil, propertyMapComputed, tfs, ps, added()) + }) + + t.Run("deleted", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapValue1, propertyMapNil, tfs, ps, deleted()) + }) + + if tt.emptyValue != nil { + t.Run("changed from empty", func(t *testing.T) { + expected := make(map[string]*pulumirpc.PropertyDiff) + if tt.listLike { + expected["foo[0]"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD} + } else if tt.objectLike { + expected["foo.foo"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_ADD} + } else { + expected["foo"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + } + runDetailedDiffTest(t, propertyMapEmpty, propertyMapValue1, tfs, ps, expected) + }) + + t.Run("changed to empty", func(t *testing.T) { + expected := make(map[string]*pulumirpc.PropertyDiff) + if tt.listLike { + expected["foo[0]"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE} + } else if tt.objectLike { + expected["foo.foo"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_DELETE} + } else { + expected["foo"] = &pulumirpc.PropertyDiff{Kind: pulumirpc.PropertyDiff_UPDATE} + } + runDetailedDiffTest(t, propertyMapValue1, propertyMapEmpty, tfs, ps, expected) + }) + + t.Run("changed from empty to computed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputed, tfs, ps, updated()) + }) + + t.Run("unchanged empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("deleted() empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapNil, tfs, ps, deleted()) + }) + + t.Run("added() empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapNil, propertyMapEmpty, tfs, ps, added()) + }) + } + }) + } +} + +func TestDetailedDiffObject(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "prop1": {Type: schema.TypeString}, + "prop2": {Type: schema.TypeString}, + }, + }, + MaxItems: 1, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{}, + }, + ) + propertyMapProp1Val1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"prop1": "val1"}, + }, + ) + propertyMapProp1Val2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"prop1": "val2"}, + }, + ) + propertyMapProp2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"prop2": "qux"}, + }, + ) + propertyMapBothProps := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"prop1": "val1", "prop2": "qux"}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapProp1Val1, propertyMapProp1Val1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapProp1Val1, propertyMapProp1Val2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.prop1": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapProp1Val1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.prop1": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed from empty both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapBothProps, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.prop1": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo.prop2": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("removed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapBothProps, propertyMapProp1Val1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.prop2": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("one added one removed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapProp1Val1, propertyMapProp2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.prop1": {Kind: pulumirpc.PropertyDiff_DELETE}, + "foo.prop2": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("added non empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapProp1Val1, propertyMapBothProps, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.prop2": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) +} + +func TestDetailedDiffList(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{}, + }, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{"val1"}, + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{"val2"}, + }, + ) + propertyMapBoth := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{"val1", "val2"}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed from empty to both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapBoth, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("removed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapBoth, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapBoth, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) +} + +func TestDetailedDiffMap(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{}, + }, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"key1": "val1"}, + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"key1": "val2"}, + }, + ) + propertyMapBoth := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"key1": "val1", "key2": "val2"}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.key1": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.key1": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed from empty to both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapBoth, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.key1": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo.key2": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("removed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapBoth, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.key2": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapBoth, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.key1": {Kind: pulumirpc.PropertyDiff_DELETE}, + "foo.key2": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) +} + +func TestDetailedDiffSet(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{}, + }, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{"val1"}, + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{"val2"}, + }, + ) + propertyMapBoth := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []interface{}{"val1", "val2"}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed from empty to both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapBoth, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("removed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapBoth, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapBoth, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("added", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapBoth, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("added both", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapBoth, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) +} + +func TestDetailedDiffTFForceNewPlain(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "string_prop": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "string_prop": "val1", + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "string_prop": "val2", + }, + ) + computedPropertyMap := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "string_prop": ComputedVal, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }) + }) + + t.Run("changed to computed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, computedPropertyMap, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed empty to computed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, computedPropertyMap, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) +} + +func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) { + for _, tt := range []struct { + name string + schema *schema.Schema + elementIndex string + emptyValue interface{} + value1 interface{} + value2 interface{} + computedCollection interface{} + computedElem interface{} + }{ + { + name: "list", + schema: &schema.Schema{ + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + ForceNew: true, + }, + elementIndex: "prop[0]", + value1: []interface{}{"val1"}, + value2: []interface{}{"val2"}, + computedCollection: ComputedVal, + computedElem: []interface{}{ComputedVal}, + }, + { + name: "set", + schema: &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + ForceNew: true, + }, + elementIndex: "prop[0]", + value1: []interface{}{"val1"}, + value2: []interface{}{"val2"}, + computedCollection: ComputedVal, + computedElem: []interface{}{ComputedVal}, + }, + { + name: "map", + schema: &schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + ForceNew: true, + }, + elementIndex: "prop.key", + value1: map[string]interface{}{"key": "val1"}, + value2: map[string]interface{}{"key": "val2"}, + computedCollection: ComputedVal, + computedElem: map[string]interface{}{"key": ComputedVal}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "prop": tt.schema, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapListVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "prop": tt.value1, + }, + ) + propertyMapListVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "prop": tt.value2, + }, + ) + propertyMapComputedCollection := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "prop": tt.computedCollection, + }, + ) + propertyMapComputedElem := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "prop": tt.computedElem, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapListVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapListVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + tt.elementIndex: {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapListVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "prop": {Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }) + }) + + t.Run("changed to computed collection", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedCollection, tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "prop": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + tt.elementIndex: {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed from empty to computed collection", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedCollection, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed from empty to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + }) + } +} + +func TestDetailedDiffTFForceNewBlockCollection(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "list_prop": { + ForceNew: true, + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{"key": { + Type: schema.TypeString, + Optional: true, + }}, + }, + Optional: true, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + + propertyMapListVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{map[string]interface{}{"key": "val1"}}, + }, + ) + + propertyMapListVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{map[string]interface{}{"key": "val2"}}, + }, + ) + propertyMapComputedCollection := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": ComputedVal, + }, + ) + propertyMapComputedElem := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{computedValue}, + }, + ) + propertyMapComputedElemProp := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{map[string]interface{}{"key": ComputedVal}}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapListVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapListVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop[0].key": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapListVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }) + }) + + t.Run("changed to computed collection", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedCollection, tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedElem, tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "list_prop[0]": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed to computed elem prop", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedElemProp, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop[0].key": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty to computed collection", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedCollection, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed from empty to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed from empty to computed elem prop", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElemProp, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) +} + +func TestDetailedDiffTFForceNewElemBlockCollection(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "list_prop": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{"key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }}, + }, + Optional: true, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + + propertyMapListVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{map[string]interface{}{"key": "val1"}}, + }, + ) + + propertyMapListVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{map[string]interface{}{"key": "val2"}}, + }, + ) + + propertyMapComputedCollection := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": ComputedVal, + }, + ) + + propertyMapComputedElem := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{computedValue}, + }, + ) + + propertyMapComputedElemProp := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "list_prop": []interface{}{map[string]interface{}{"key": ComputedVal}}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapListVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapListVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop[0].key": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapListVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }) + }) + + t.Run("changed to computed collection", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedCollection, tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedElem, tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "list_prop[0]": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed to computed elem prop", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapListVal1, propertyMapComputedElemProp, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop[0].key": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + // Note this might actually lead to a replacement, but we don't have enough information to know that. + t.Run("changed from empty to computed collection", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedCollection, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + // Note this might actually lead to a replacement, but we don't have enough information to know that. + t.Run("changed from empty to computed elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed from empty to computed elem prop", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElemProp, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "list_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) +} + +func TestDetailedDiffMaxItemsOnePlainType(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "string_prop": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "string_prop": "val1", + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "string_prop": "val2", + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("changed to computed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, + resource.NewPropertyMapFromMap(map[string]interface{}{"string_prop": ComputedVal}), tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) +} + +func TestDetailedDiffNestedMaxItemsOnePlainType(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "string_prop": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "string_prop": "val1", + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "string_prop": "val2", + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("changed to computed", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, + resource.NewPropertyMapFromMap(map[string]interface{}{"string_prop": ComputedVal}), tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "string_prop": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) +} + +func TestDetailedDiffTFForceNewObject(t *testing.T) { + // Note that maxItemsOne flattening means that the PropertyMap values contain no lists + sdkv2Schema := map[string]*schema.Schema{ + "object_prop": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + Optional: true, + MaxItems: 1, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapObjectVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "object_prop": map[string]interface{}{"key": "val1"}, + }, + ) + propertyMapObjectVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "object_prop": map[string]interface{}{"key": "val2"}, + }, + ) + + propertyMapComputedObject := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "object_prop": ComputedVal, + }, + ) + + propertyMapComputedObjectProp := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "object_prop": map[string]interface{}{"key": ComputedVal}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapObjectVal1, propertyMapObjectVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapObjectVal1, propertyMapObjectVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "object_prop.key": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapObjectVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "object_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapObjectVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "object_prop": {Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }) + }) + + t.Run("changed to computed object", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapObjectVal1, propertyMapComputedObject, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "object_prop": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed to computed object prop", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapObjectVal1, propertyMapComputedObjectProp, tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "object_prop.key": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + // Note this might actually lead to a replacement, but we don't have enough information to know that. + t.Run("changed from empty to computed object", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedObject, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "object_prop": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed from empty to computed object prop", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedObjectProp, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "object_prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) +} + +func TestDetailedDiffPulumiSchemaOverride(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeString, + Optional: true, + }, + } + t.Run("renamed property", func(t *testing.T) { + tfs := shimv2.NewSchemaMap(sdkv2Schema) + ps := map[string]*SchemaInfo{ + "foo": { + Name: "bar", + }, + } + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "bar": "val1", + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "bar": "val2", + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "bar": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "bar": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "bar": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + }) + + t.Run("force new override property", func(t *testing.T) { + tfs := shimv2.NewSchemaMap(sdkv2Schema) + ps := map[string]*SchemaInfo{ + "foo": { + ForceNew: True(), + }, + } + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": "val1", + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": "val2", + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE_REPLACE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_DELETE_REPLACE}, + }) + }) + }) + + t.Run("Type override property", func(t *testing.T) { + tfs := shimv2.NewSchemaMap(sdkv2Schema) + ps := map[string]*SchemaInfo{ + "foo": { + Type: "number", + }, + } + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": 1, + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": 2, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, updated()) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, added()) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, deleted()) + }) + }) + + t.Run("max items one override property", func(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bar": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + } + tfs := shimv2.NewSchemaMap(sdkv2Schema) + ps := map[string]*SchemaInfo{ + "foo": { + MaxItemsOne: True(), + }, + } + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"bar": "val1"}, + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": map[string]interface{}{"bar": "val2"}, + }, + ) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo.bar": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + }) + + t.Run("max items one removed override property", func(t *testing.T) { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bar": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + } + tfs := shimv2.NewSchemaMap(sdkv2Schema) + ps := map[string]*SchemaInfo{ + "foo": { + MaxItemsOne: False(), + }, + } + + propertyMapEmpty := resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ) + propertyMapVal1 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []map[string]interface{}{{"bar": "val1"}}, + }, + ) + propertyMapVal2 := resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": []map[string]interface{}{{"bar": "val2"}}, + }, + ) + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapVal2, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0].bar": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapVal1, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapVal1, propertyMapEmpty, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + }) +} diff --git a/pkg/tfbridge/provider.go b/pkg/tfbridge/provider.go index 4ef8f5f06..1e96af03c 100644 --- a/pkg/tfbridge/provider.go +++ b/pkg/tfbridge/provider.go @@ -1140,7 +1140,7 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum schema, fields := res.TF.Schema(), res.Schema.Fields - config, _, err := MakeTerraformConfig(ctx, p, news, schema, fields) + config, assets, err := MakeTerraformConfig(ctx, p, news, schema, fields) if err != nil { return nil, errors.Wrapf(err, "preparing %s's new property state", urn) } @@ -1158,18 +1158,24 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum return nil, errors.Wrapf(err, "diffing %s", urn) } - dd := makeDetailedDiffExtra(ctx, schema, fields, olds, news, diff) - detailedDiff, changes := dd.diffs, dd.changes + var detailedDiff map[string]*pulumirpc.PropertyDiff + var changes pulumirpc.DiffResponse_DiffChanges - if opts.enableAccurateBridgePreview { - if decision := diff.DiffEqualDecisionOverride(); decision != shim.DiffNoOverride { - if decision == shim.DiffOverrideNoUpdate { - changes = pulumirpc.DiffResponse_DIFF_NONE - } else { - changes = pulumirpc.DiffResponse_DIFF_SOME - } + decisionOverride := diff.DiffEqualDecisionOverride() + if opts.enableAccurateBridgePreview && decisionOverride != shim.DiffNoOverride { + if decisionOverride == shim.DiffOverrideNoUpdate { + changes = pulumirpc.DiffResponse_DIFF_NONE + } else { + changes = pulumirpc.DiffResponse_DIFF_SOME + } + + detailedDiff, err = makeDetailedDiffV2(ctx, schema, fields, res.TF, p.tf, state, diff, assets, p.supportsSecrets) + if err != nil { + return nil, err } } else { + dd := makeDetailedDiffExtra(ctx, schema, fields, olds, news, diff) + detailedDiff, changes = dd.diffs, dd.changes // There are some providers/situations which `makeDetailedDiff` distorts the expected changes, leading // to changes being dropped by Pulumi. // Until we fix `makeDetailedDiff`, it is safer to refer to the Terraform Diff attribute length for setting @@ -1180,12 +1186,12 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum if !diff.HasNoChanges() { changes = pulumirpc.DiffResponse_DIFF_SOME } - } - if changes == pulumirpc.DiffResponse_DIFF_SOME { - // Perhaps collectionDiffs can shed some light and locate the changes to the end-user. - for path, diff := range dd.collectionDiffs { - detailedDiff[path] = diff + if changes == pulumirpc.DiffResponse_DIFF_SOME { + // Perhaps collectionDiffs can shed some light and locate the changes to the end-user. + for path, diff := range dd.collectionDiffs { + detailedDiff[path] = diff + } } } @@ -1231,19 +1237,21 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum deleteBeforeReplace := len(replaces) > 0 && (res.Schema.DeleteBeforeReplace || nameRequiresDeleteBeforeReplace(news, olds, schema, res.Schema)) - // If the upstream diff object indicates a replace is necessary and we have not - // recorded any replaces, that means that `makeDetailedDiff` failed to translate a - // property. This is known to happen for computed input properties: - // - // https://github.com/pulumi/pulumi-aws/issues/2971 - if (diff.RequiresNew() || diff.Destroy()) && - // In theory, we should be safe to set __meta as replaces whenever - // `diff.RequiresNew() || diff.Destroy()` but by checking replaces we - // limit the blast radius of this change to diffs that we know will panic - // later on. - len(replaces) == 0 { - replaces = append(replaces, "__meta") - changes = pulumirpc.DiffResponse_DIFF_SOME + if !opts.enableAccurateBridgePreview { + // If the upstream diff object indicates a replace is necessary and we have not + // recorded any replaces, that means that `makeDetailedDiff` failed to translate a + // property. This is known to happen for computed input properties: + // + // https://github.com/pulumi/pulumi-aws/issues/2971 + if (diff.RequiresNew() || diff.Destroy()) && + // In theory, we should be safe to set __meta as replaces whenever + // `diff.RequiresNew() || diff.Destroy()` but by checking replaces we + // limit the blast radius of this change to diffs that we know will panic + // later on. + len(replaces) == 0 { + replaces = append(replaces, "__meta") + changes = pulumirpc.DiffResponse_DIFF_SOME + } } if changes == pulumirpc.DiffResponse_DIFF_NONE && diff --git a/pkg/tfshim/sdk-v1/instance_diff.go b/pkg/tfshim/sdk-v1/instance_diff.go index 6c6d39c19..aee9c683b 100644 --- a/pkg/tfshim/sdk-v1/instance_diff.go +++ b/pkg/tfshim/sdk-v1/instance_diff.go @@ -1,6 +1,7 @@ package sdkv1 import ( + "fmt" "strings" "time" @@ -89,6 +90,10 @@ func (d v1InstanceDiff) ProposedState(res shim.Resource, priorState shim.Instanc return v1InstanceState{tf: prior, diff: d.tf}, nil } +func (d v1InstanceDiff) PriorState() (shim.InstanceState, error) { + return nil, fmt.Errorf("prior state is not available") +} + func (d v1InstanceDiff) Destroy() bool { return d.tf.Destroy } diff --git a/pkg/tfshim/sdk-v2/instance_diff.go b/pkg/tfshim/sdk-v2/instance_diff.go index a2ffe8350..f9f4b3c27 100644 --- a/pkg/tfshim/sdk-v2/instance_diff.go +++ b/pkg/tfshim/sdk-v2/instance_diff.go @@ -1,6 +1,7 @@ package sdkv2 import ( + "fmt" "strings" "time" @@ -84,6 +85,10 @@ func (d v2InstanceDiff) ProposedState(res shim.Resource, priorState shim.Instanc }, nil } +func (d v2InstanceDiff) PriorState() (shim.InstanceState, error) { + return nil, fmt.Errorf("prior state is not available") +} + func (d v2InstanceDiff) Destroy() bool { return d.tf.Destroy } diff --git a/pkg/tfshim/sdk-v2/provider2.go b/pkg/tfshim/sdk-v2/provider2.go index 0e245f4a4..ef6e33438 100644 --- a/pkg/tfshim/sdk-v2/provider2.go +++ b/pkg/tfshim/sdk-v2/provider2.go @@ -116,6 +116,8 @@ type v2InstanceDiff2 struct { plannedState cty.Value plannedPrivate map[string]interface{} diffEqualDecisionOverride shim.DiffOverride + prior cty.Value + priorMeta map[string]interface{} } func (d *v2InstanceDiff2) String() string { @@ -147,6 +149,13 @@ func (d *v2InstanceDiff2) ProposedState( }, nil } +func (d *v2InstanceDiff2) PriorState() (shim.InstanceState, error) { + return &v2InstanceState2{ + stateValue: d.prior, + meta: d.priorMeta, + }, nil +} + func (d *v2InstanceDiff2) DiffEqualDecisionOverride() shim.DiffOverride { return d.diffEqualDecisionOverride } @@ -299,6 +308,8 @@ func (p *planResourceChangeImpl) Diff( plannedState: plannedState, diffEqualDecisionOverride: diffOverride, plannedPrivate: plan.PlannedPrivate, + prior: st, + priorMeta: priv, }, err } diff --git a/pkg/tfshim/shim.go b/pkg/tfshim/shim.go index b2a2632d1..8bf69dfae 100644 --- a/pkg/tfshim/shim.go +++ b/pkg/tfshim/shim.go @@ -63,6 +63,8 @@ type InstanceDiff interface { // // DiffEqualDecisionOverride is only respected when EnableAccurateBridgePreview is set. DiffEqualDecisionOverride() DiffOverride + // Required if DiffEqualDecisionOverride is enabled. + PriorState() (InstanceState, error) } type ValueType int diff --git a/pkg/tfshim/tfplugin5/instance_diff.go b/pkg/tfshim/tfplugin5/instance_diff.go index c8e0d397c..d956be8c2 100644 --- a/pkg/tfshim/tfplugin5/instance_diff.go +++ b/pkg/tfshim/tfplugin5/instance_diff.go @@ -90,6 +90,10 @@ func (d *instanceDiff) ProposedState(res shim.Resource, priorState shim.Instance }, nil } +func (d *instanceDiff) PriorState() (shim.InstanceState, error) { + return nil, fmt.Errorf("prior state is not available") +} + func (d *instanceDiff) Destroy() bool { return d.destroy }