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 }