diff --git a/dynamic/go.sum b/dynamic/go.sum index 29ca11add..1b9df93a7 100644 --- a/dynamic/go.sum +++ b/dynamic/go.sum @@ -758,8 +758,8 @@ github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= github.com/pulumi/inflector v0.1.1 h1:dvlxlWtXwOJTUUtcYDvwnl6Mpg33prhK+7mzeF+SobA= github.com/pulumi/inflector v0.1.1/go.mod h1:HUFCjcPTz96YtTuUlwG3i3EZG4WlniBvR9bd+iJxCUY= -github.com/pulumi/providertest v0.1.2 h1:9pJS9MeNkMyGwyNeHmvh8QqLgJy39Nk2/ym5u7r13ng= -github.com/pulumi/providertest v0.1.2/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= +github.com/pulumi/providertest v0.1.3 h1:GpNKRy/haNjRHiUA9bi4diU4Op2zf3axYXbga5AepHg= +github.com/pulumi/providertest v0.1.3/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= github.com/pulumi/pulumi-java/pkg v0.16.1 h1:orHnDWFbpOERwaBLry9f+6nqPX7x0MsrIkaa5QDGAns= github.com/pulumi/pulumi-java/pkg v0.16.1/go.mod h1:QH0DihZkWYle9XFc+LJ76m4hUo+fA3RdyaM90pqOaSM= github.com/pulumi/pulumi-yaml v1.10.3 h1:j5cjPiE32ILmjrWnC1cfZ0MWdqCZ8fg9wlaWk7HOtM4= diff --git a/go.mod b/go.mod index f1e6acff0..32edee24e 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 github.com/pkg/errors v0.9.1 github.com/pulumi/inflector v0.1.1 - github.com/pulumi/providertest v0.1.2 + github.com/pulumi/providertest v0.1.3 github.com/pulumi/pulumi-java/pkg v0.16.1 github.com/pulumi/pulumi-yaml v1.10.3 github.com/pulumi/schema-tools v0.1.2 diff --git a/go.sum b/go.sum index 62d7b9a2c..974a3b276 100644 --- a/go.sum +++ b/go.sum @@ -1932,8 +1932,8 @@ github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= github.com/pulumi/inflector v0.1.1 h1:dvlxlWtXwOJTUUtcYDvwnl6Mpg33prhK+7mzeF+SobA= github.com/pulumi/inflector v0.1.1/go.mod h1:HUFCjcPTz96YtTuUlwG3i3EZG4WlniBvR9bd+iJxCUY= -github.com/pulumi/providertest v0.1.2 h1:9pJS9MeNkMyGwyNeHmvh8QqLgJy39Nk2/ym5u7r13ng= -github.com/pulumi/providertest v0.1.2/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= +github.com/pulumi/providertest v0.1.3 h1:GpNKRy/haNjRHiUA9bi4diU4Op2zf3axYXbga5AepHg= +github.com/pulumi/providertest v0.1.3/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= github.com/pulumi/pulumi-java/pkg v0.16.1 h1:orHnDWFbpOERwaBLry9f+6nqPX7x0MsrIkaa5QDGAns= github.com/pulumi/pulumi-java/pkg v0.16.1/go.mod h1:QH0DihZkWYle9XFc+LJ76m4hUo+fA3RdyaM90pqOaSM= github.com/pulumi/pulumi-yaml v1.10.3 h1:j5cjPiE32ILmjrWnC1cfZ0MWdqCZ8fg9wlaWk7HOtM4= diff --git a/pf/go.sum b/pf/go.sum index a7e2f834a..ff5a6f9ad 100644 --- a/pf/go.sum +++ b/pf/go.sum @@ -731,8 +731,8 @@ github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= github.com/pulumi/inflector v0.1.1 h1:dvlxlWtXwOJTUUtcYDvwnl6Mpg33prhK+7mzeF+SobA= github.com/pulumi/inflector v0.1.1/go.mod h1:HUFCjcPTz96YtTuUlwG3i3EZG4WlniBvR9bd+iJxCUY= -github.com/pulumi/providertest v0.1.2 h1:9pJS9MeNkMyGwyNeHmvh8QqLgJy39Nk2/ym5u7r13ng= -github.com/pulumi/providertest v0.1.2/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= +github.com/pulumi/providertest v0.1.3 h1:GpNKRy/haNjRHiUA9bi4diU4Op2zf3axYXbga5AepHg= +github.com/pulumi/providertest v0.1.3/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= github.com/pulumi/pulumi-java/pkg v0.16.1 h1:orHnDWFbpOERwaBLry9f+6nqPX7x0MsrIkaa5QDGAns= github.com/pulumi/pulumi-java/pkg v0.16.1/go.mod h1:QH0DihZkWYle9XFc+LJ76m4hUo+fA3RdyaM90pqOaSM= github.com/pulumi/pulumi-yaml v1.10.3 h1:j5cjPiE32ILmjrWnC1cfZ0MWdqCZ8fg9wlaWk7HOtM4= diff --git a/pkg/tests/schema_pulumi_test.go b/pkg/tests/schema_pulumi_test.go index f37eb33e4..c411549d5 100644 --- a/pkg/tests/schema_pulumi_test.go +++ b/pkg/tests/schema_pulumi_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "time" @@ -2303,9 +2304,7 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [0]: "val2" => "val1" - ~ [1]: "val3" => "val2" - + [2]: "val3" + + [0]: "val1" ] Resources: ~ 1 to update @@ -2341,8 +2340,7 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [1]: "val3" => "val2" - + [2]: "val3" + + [1]: "val2" ] Resources: ~ 1 to update @@ -2360,9 +2358,7 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [0]: "val1" => "val2" - ~ [1]: "val2" => "val3" - - [2]: "val3" + - [0]: "val1" ] Resources: ~ 1 to update @@ -2398,8 +2394,7 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setProps: [ - ~ [1]: "val2" => "val3" - - [2]: "val3" + - [1]: "val2" ] Resources: ~ 1 to update @@ -3019,14 +3014,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - ~ prop: "val2" => "val1" - } - ~ [1]: { - ~ prop: "val3" => "val2" - } - + [2]: { - + prop : "val3" + + [0]: { + + prop : "val1" } ] Resources: @@ -3079,11 +3068,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [1]: { - ~ prop: "val3" => "val2" - } - + [2]: { - + prop : "val3" + + [1]: { + + prop : "val2" } ] Resources: @@ -3109,14 +3095,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [0]: { - ~ prop: "val1" => "val2" - } - ~ [1]: { - ~ prop: "val2" => "val3" - } - - [2]: { - - prop: "val3" + - [0]: { + - prop: "val1" } ] Resources: @@ -3169,11 +3149,8 @@ Resources: [id=newid] [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] ~ setBlocks: [ - ~ [1]: { - ~ prop: "val2" => "val3" - } - - [2]: { - - prop: "val3" + - [1]: { + - prop: "val2" } ] Resources: @@ -3979,130 +3956,6 @@ outputs: assert.Equal(t, "", out.Outputs["emptyValue"].Value) } -func TestUnknownSetElementDiff(t *testing.T) { - resMap := map[string]*schema.Resource{ - "prov_test": { - Schema: map[string]*schema.Schema{ - "test": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - "prov_aux": { - Schema: map[string]*schema.Schema{ - "aux": { - Type: schema.TypeString, - Computed: true, - Optional: true, - }, - }, - CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("aux") - err := d.Set("aux", "aux") - require.NoError(t, err) - return nil - }, - }, - } - tfp := &schema.Provider{ResourcesMap: resMap} - - runTest := func(t *testing.T, PRC bool, expectedOutput autogold.Value) { - opts := []pulcheck.BridgedProviderOpt{} - if !PRC { - opts = append(opts, pulcheck.DisablePlanResourceChange()) - } - bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp, opts...) - originalProgram := ` -name: test -runtime: yaml -resources: - mainRes: - type: prov:index:Test -outputs: - testOut: ${mainRes.tests} - ` - - programWithUnknown := ` -name: test -runtime: yaml -resources: - auxRes: - type: prov:index:Aux - mainRes: - type: prov:index:Test - properties: - tests: - - ${auxRes.aux} -outputs: - testOut: ${mainRes.tests} -` - pt := pulcheck.PulCheck(t, bridgedProvider, originalProgram) - pt.Up(t) - pulumiYamlPath := filepath.Join(pt.CurrentStack().Workspace().WorkDir(), "Pulumi.yaml") - - err := os.WriteFile(pulumiYamlPath, []byte(programWithUnknown), 0o600) - require.NoError(t, err) - - res := pt.Preview(t, optpreview.Diff()) - // Test that the test property is unknown at preview time - expectedOutput.Equal(t, res.StdOut) - resUp := pt.Up(t) - // assert that the property gets resolved - require.Equal(t, - []interface{}{"aux"}, - resUp.Outputs["testOut"].Value, - ) - } - - t.Run("PRC enabled", func(t *testing.T) { - // 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] - + prov:index/aux:Aux: (create) - [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - + tests: [ - + [0]: output - ] - --outputs:-- - + testOut: output -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged -`)) - }) - - t.Run("PRC disabled", func(t *testing.T) { - runTest(t, false, autogold.Expect(`Previewing update (test): - pulumi:pulumi:Stack: (same) - [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] - + prov:index/aux:Aux: (create) - [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] - ~ prov:index/test:Test: (update) - [id=newid] - [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] - + tests: [ - + [0]: output - ] - --outputs:-- - + testOut: output -Resources: - + 1 to create - ~ 1 to update - 2 changes. 1 unchanged -`)) - }) -} - func TestMakeTerraformResultNilVsEmptyMap(t *testing.T) { // Nil and empty maps are not equal nilMap := resource.NewObjectProperty(nil) @@ -4159,3 +4012,3298 @@ func TestMakeTerraformResultNilVsEmptyMap(t *testing.T) { assert.True(t, props["test"].DeepEquals(emptyMap)) }) } + +func runDetailedDiffTest( + t *testing.T, resMap map[string]*schema.Resource, program1, program2 string, +) (string, map[string]interface{}) { + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + pt := pulcheck.PulCheck(t, bridgedProvider, program1) + pt.Up(t) + pulumiYamlPath := filepath.Join(pt.CurrentStack().Workspace().WorkDir(), "Pulumi.yaml") + + err := os.WriteFile(pulumiYamlPath, []byte(program2), 0o600) + require.NoError(t, err) + + pt.ClearGrpcLog(t) + res := pt.Preview(t, optpreview.Diff()) + t.Log(res.StdOut) + + diffResponse := struct { + DetailedDiff map[string]interface{} `json:"detailedDiff"` + }{} + + for _, entry := range pt.GrpcLog(t).Entries { + if entry.Method == "/pulumirpc.ResourceProvider/Diff" { + err := json.Unmarshal(entry.Response, &diffResponse) + require.NoError(t, err) + } + } + + return res.StdOut, diffResponse.DetailedDiff +} + +func TestDetailedDiffSet(t *testing.T) { + // TODO: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + runTest := func(t *testing.T, resMap map[string]*schema.Resource, props1, props2 interface{}, + expected autogold.Value, expectedDetailedDiff map[string]any, + ) { + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s +` + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + program1 := fmt.Sprintf(program, string(props1JSON)) + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + program2 := fmt.Sprintf(program, string(props2JSON)) + out, detailedDiff := runDetailedDiffTest(t, resMap, program1, program2) + + expected.Equal(t, out) + require.Equal(t, expectedDetailedDiff, detailedDiff) + } + + type setDetailedDiffTestCase struct { + name string + props1 []string + props2 []string + expectedAttrDetailedDiff map[string]any + expectedAttr autogold.Value + expectedAttrForceNewDetailedDiff map[string]any + expectedAttrForceNew autogold.Value + expectedBlockDetailedDiff map[string]any + expectedBlock autogold.Value + expectedBlockForceNewDetailedDiff map[string]any + expectedBlockForceNew autogold.Value + } + + testCases := []setDetailedDiffTestCase{ + { + "unchanged", + []string{"val1"}, + []string{"val1"}, + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + }, + { + "changed non-empty", + []string{"val1"}, + []string{"val2"}, + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [0]: "val1" => "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[0].nested": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [0]: { + ~ nested: "val1" => "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: { + ~ nested: "val1" => "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "changed from empty", + []string{}, + []string{"val1"}, + map[string]interface{}{"tests": map[string]interface{}{}}, + 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] + + tests: [ + + [0]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests": map[string]interface{}{}}, + 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] + + tests: [ + + [0]: { + + nested : "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: { + + nested : "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "changed to empty", + []string{"val1"}, + []string{}, + map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE"}}, + 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] + - tests: [ + - [0]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE"}}, + 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] + - tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "removed front", + []string{"val1", "val2", "val3"}, + []string{"val2", "val3"}, + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [0]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "removed front unordered", + []string{"val2", "val1", "val3"}, + []string{"val1", "val3"}, + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [1]: "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "removed middle", + []string{"val1", "val2", "val3"}, + []string{"val1", "val3"}, + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [1]: "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "removed middle unordered", + []string{"val2", "val3", "val1"}, + []string{"val2", "val1"}, + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "removed end", + []string{"val1", "val2", "val3"}, + []string{"val1", "val2"}, + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "removed end unordered", + []string{"val2", "val3", "val1"}, + []string{"val2", "val3"}, + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [0]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "added front", + []string{"val2", "val3"}, + []string{"val1", "val2", "val3"}, + map[string]interface{}{"tests[0]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [0]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "added front unordered", + []string{"val3", "val1"}, + []string{"val2", "val2", "val1"}, + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [1]: "val3" => "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: "val3" => "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [1]: { + ~ nested: "val3" => "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: { + ~ nested: "val3" => "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "added middle", + []string{"val1", "val3"}, + []string{"val1", "val2", "val3"}, + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "added middle unordered", + []string{"val2", "val1"}, + []string{"val2", "val3", "val1"}, + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "added end", + []string{"val1", "val2"}, + []string{"val1", "val2", "val3"}, + map[string]interface{}{"tests[2]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "added end unordered", + []string{"val2", "val3"}, + []string{"val2", "val3", "val1"}, + map[string]interface{}{"tests[2]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: { + + nested : "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "same element updated", + []string{"val1", "val2", "val3"}, + []string{"val1", "val4", "val3"}, + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [1]: "val2" => "val4" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: "val2" => "val4" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [1]: { + ~ nested: "val2" => "val4" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [1]: { + ~ nested: "val2" => "val4" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "same element updated unordered", + []string{"val2", "val3", "val1"}, + []string{"val2", "val4", "val1"}, + map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [1]: "val4" + - [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val4" + - [2]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [1]: { + + nested : "val4" + } + - [2]: { + - nested: "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val4" + } + - [2]: { + - nested: "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled", + []string{"val1", "val2", "val3"}, + []string{"val3", "val1", "val2"}, + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + }, + { + "shuffled unordered", + []string{"val2", "val3", "val1"}, + []string{"val3", "val1", "val2"}, + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + }, + { + "shuffled with duplicates", + []string{"val1", "val2", "val3"}, + []string{"val3", "val1", "val2", "val3"}, + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + }, + { + "shuffled with duplicates unordered", + []string{"val2", "val3", "val1"}, + []string{"val3", "val1", "val2", "val3"}, + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + nil, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] +Resources: + 2 unchanged +`), + }, + { + "shuffled added front", + []string{"val2", "val3"}, + []string{"val1", "val3", "val2"}, + map[string]interface{}{"tests[0]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [0]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled added front unordered", + []string{"val3", "val1"}, + []string{"val2", "val1", "val3"}, + map[string]interface{}{"tests[0]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [0]: "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [0]: { + + nested : "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled added middle", + []string{"val1", "val3"}, + []string{"val3", "val2", "val1"}, + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled added middle unordered", + []string{"val2", "val1"}, + []string{"val1", "val3", "val2"}, + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled added end", + []string{"val1", "val2"}, + []string{"val2", "val1", "val3"}, + map[string]interface{}{"tests[2]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled removed front", + []string{"val1", "val2", "val3"}, + []string{"val3", "val2"}, + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [0]: "val1" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: "val1" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[0]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [0]: { + - nested: "val1" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled removed middle", + []string{"val1", "val2", "val3"}, + []string{"val3", "val1"}, + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [1]: "val2" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: "val2" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[1]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [1]: { + - nested: "val2" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "shuffled removed end", + []string{"val1", "val2", "val3"}, + []string{"val2", "val1"}, + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: "val3" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "two added", + []string{"val1", "val2"}, + []string{"val1", "val2", "val3", "val4"}, + map[string]interface{}{"tests[2]": map[string]interface{}{}, "tests[3]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: "val3" + + [3]: "val4" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: "val3" + + [3]: "val4" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{}, "tests[3]": map[string]interface{}{}}, + 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] + ~ tests: [ + + [2]: { + + nested : "val3" + } + + [3]: { + + nested : "val4" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "ADD_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "ADD_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [2]: { + + nested : "val3" + } + + [3]: { + + nested : "val4" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "two removed", + []string{"val1", "val2", "val3", "val4"}, + []string{"val1", "val2"}, + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}, "tests[3]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: "val3" + - [3]: "val4" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: "val3" + - [3]: "val4" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE"}, "tests[3]": map[string]interface{}{"kind": "DELETE"}}, + 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] + ~ tests: [ + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "two added and two removed", + []string{"val1", "val2", "val3", "val4"}, + []string{"val1", "val2", "val5", "val6"}, + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "UPDATE"}, "tests[3]": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [2]: "val3" => "val5" + ~ [3]: "val4" => "val6" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2]": map[string]interface{}{"kind": "UPDATE_REPLACE"}, "tests[3]": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [2]: "val3" => "val5" + ~ [3]: "val4" => "val6" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{"tests[2].nested": map[string]interface{}{"kind": "UPDATE"}, "tests[3].nested": map[string]interface{}{"kind": "UPDATE"}}, + 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] + ~ tests: [ + ~ [2]: { + ~ nested: "val3" => "val5" + } + ~ [3]: { + ~ nested: "val4" => "val6" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{"tests[2].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}, "tests[3].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [2]: { + ~ nested: "val3" => "val5" + } + ~ [3]: { + ~ nested: "val4" => "val6" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "two added and two removed shuffled, one overlaps", + []string{"val1", "val2", "val3", "val4"}, + []string{"val1", "val5", "val6", "val2"}, + map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "two added and two removed shuffled, no overlaps", + []string{"val1", "val2", "val3", "val4"}, + []string{"val5", "val6", "val1", "val2"}, + map[string]interface{}{ + "tests[0]": map[string]interface{}{}, + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [0]: "val5" + + [1]: "val6" + - [2]: "val3" + - [3]: "val4" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: "val5" + + [1]: "val6" + - [2]: "val3" + - [3]: "val4" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{ + "tests[0]": map[string]interface{}{}, + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "DELETE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [0]: { + + nested : "val5" + } + + [1]: { + + nested : "val6" + } + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[0]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "val5" + } + + [1]: { + + nested : "val6" + } + - [2]: { + - nested: "val3" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + { + "two added and two removed shuffled, with duplicates", + []string{"val1", "val2", "val3", "val4"}, + []string{"val1", "val5", "val6", "val2", "val1", "val2"}, + map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2]": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2]": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: "val5" + ~ [2]: "val3" => "val6" + - [3]: "val4" + ] +Resources: + +-1 to replace + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE"}, + }, + 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] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`), + map[string]interface{}{ + "tests[1]": map[string]interface{}{"kind": "ADD_REPLACE"}, + "tests[2].nested": map[string]interface{}{"kind": "UPDATE_REPLACE"}, + "tests[3]": map[string]interface{}{"kind": "DELETE_REPLACE"}, + }, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + +-prov:index/test:Test: (replace) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [1]: { + + nested : "val5" + } + ~ [2]: { + ~ nested: "val3" => "val6" + } + - [3]: { + - nested: "val4" + } + ] +Resources: + +-1 to replace + 1 unchanged +`), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + for _, forceNew := range []bool{false, true} { + t.Run(fmt.Sprintf("ForceNew=%v", forceNew), func(t *testing.T) { + expected := tc.expectedAttr + if forceNew { + expected = tc.expectedAttrForceNew + } + + expectedDetailedDiff := tc.expectedAttrDetailedDiff + if forceNew { + expectedDetailedDiff = tc.expectedAttrForceNewDetailedDiff + } + t.Run("Attribute", func(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + ForceNew: forceNew, + }, + }, + } + runTest(t, map[string]*schema.Resource{"prov_test": res}, tc.props1, tc.props2, expected, expectedDetailedDiff) + }) + + expected = tc.expectedBlock + if forceNew { + expected = tc.expectedBlockForceNew + } + expectedDetailedDiff = tc.expectedBlockDetailedDiff + if forceNew { + expectedDetailedDiff = tc.expectedBlockForceNewDetailedDiff + } + + t.Run("Block", func(t *testing.T) { + res := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nested": { + Type: schema.TypeString, + Optional: true, + ForceNew: forceNew, + }, + }, + }, + }, + }, + } + + props1 := make([]interface{}, len(tc.props1)) + for i, v := range tc.props1 { + props1[i] = map[string]interface{}{"nested": v} + } + + props2 := make([]interface{}, len(tc.props2)) + for i, v := range tc.props2 { + props2[i] = map[string]interface{}{"nested": v} + } + + runTest(t, map[string]*schema.Resource{"prov_test": res}, props1, props2, expected, expectedDetailedDiff) + }) + }) + } + }) + } +} + +// "UNKNOWN" for unknown values +func testDetailedDiffWithUnknowns(t *testing.T, resMap map[string]*schema.Resource, unknownString string, props1, props2 interface{}, expected, expectedDetailedDiff autogold.Value) { + originalProgram := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s +outputs: + testOut: ${mainRes.tests} + ` + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + program1 := fmt.Sprintf(originalProgram, string(props1JSON)) + + programWithUnknown := ` +name: test +runtime: yaml +resources: + auxRes: + type: prov:index:Aux + mainRes: + type: prov:index:Test + properties: + tests: %s +outputs: + testOut: ${mainRes.tests} +` + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + program2 := fmt.Sprintf(programWithUnknown, string(props2JSON)) + program2 = strings.ReplaceAll(program2, "UNKNOWN", unknownString) + + out, detailedDiff := runDetailedDiffTest(t, resMap, program1, program2) + expected.Equal(t, out) + expectedDetailedDiff.Equal(t, detailedDiff) +} + +func TestDetailedDiffUnknownSetAttributeElement(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{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + "prov_aux": { + Schema: map[string]*schema.Schema{ + "aux": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("aux") + err := d.Set("aux", "aux") + require.NoError(t, err) + return nil + }, + }, + } + + t.Run("empty to unknown element", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{}, + []interface{}{"UNKNOWN"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: [ + + [0]: output + ] + --outputs:-- + + testOut: output +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{}})) + }) + + t.Run("non-empty to unknown element", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1"}, + []interface{}{"UNKNOWN"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => output + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}})) + }) + + t.Run("unknown element added front", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val2", "val3"}, + []interface{}{"UNKNOWN", "val2", "val3"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val2" => output + ~ [1]: "val3" => "val2" + + [2]: "val3" + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("unknown element added middle", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val3"}, + []interface{}{"val1", "UNKNOWN", "val3"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + [0]: "val1" + ~ [1]: "val3" => output + + [2]: "val3" + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("unknown element added end", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val2"}, + []interface{}{"val1", "val2", "UNKNOWN"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + [0]: "val1" + [1]: "val2" + + [2]: output + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("element updated to unknown", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val2", "val3"}, + []interface{}{"val1", "UNKNOWN", "val3"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + [0]: "val1" + ~ [1]: "val2" => output + [2]: "val3" + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("shuffled unknown added front", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val2", "val3"}, + []interface{}{"UNKNOWN", "val3", "val2"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val2" => output + [1]: "val3" + + [2]: "val2" + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("shuffled unknown added middle", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val3"}, + []interface{}{"val3", "UNKNOWN", "val1"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => "val3" + ~ [1]: "val3" => output + + [2]: "val1" + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) + + t.Run("shuffled unknown added end", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.aux}", + []interface{}{"val1", "val2"}, + []interface{}{"val2", "val1", "UNKNOWN"}, + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + ~ [0]: "val1" => "val2" + ~ [1]: "val2" => "val1" + + [2]: output + ] +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) +} + +func TestUnknownSetAttributeDiff(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{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + "prov_aux": { + Schema: map[string]*schema.Schema{ + "aux": { + Type: schema.TypeSet, + Computed: true, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("aux") + err := d.Set("aux", []interface{}{"aux"}) + require.NoError(t, err) + return nil + }, + }, + } + + t.Run("empty to unknown set", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.auxes}", + []interface{}{}, + "UNKNOWN", + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + + tests: output + --outputs:-- + + testOut: output +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{}}), + ) + }) + + t.Run("non-empty to unknown set", func(t *testing.T) { + testDetailedDiffWithUnknowns(t, resMap, "${auxRes.auxes}", + []interface{}{"val"}, + "UNKNOWN", + autogold.Expect(`Previewing update (test): + pulumi:pulumi:Stack: (same) + [urn=urn:pulumi:test::test::pulumi:pulumi:Stack::test-test] + + prov:index/aux:Aux: (create) + [urn=urn:pulumi:test::test::prov:index/aux:Aux::auxRes] + ~ prov:index/test:Test: (update) + [id=newid] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + - tests: [ + - [0]: "val" + ] + + tests: output +Resources: + + 1 to create + ~ 1 to update + 2 changes. 1 unchanged +`), + autogold.Expect(map[string]interface{}{"tests": map[string]interface{}{"kind": "UPDATE"}}), + ) + }) +} + +func TestDetailedDiffSetDuplicates(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{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + } + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s` + + t.Run("pulumi", func(t *testing.T) { + pt := pulcheck.PulCheck(t, bridgedProvider, fmt.Sprintf(program, `["a", "a"]`)) + pt.Up(t) + + pt.WritePulumiYaml(t, fmt.Sprintf(program, `["b", "b", "a", "a", "c"]`)) + + res := pt.Preview(t, optpreview.Diff()) + + 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] + ~ tests: [ + + [1]: "b" + + [4]: "c" + ] +Resources: + ~ 1 to update + 1 unchanged +`).Equal(t, res.StdOut) + }) + + t.Run("terraform", func(t *testing.T) { + tfdriver := tfcheck.NewTfDriver(t, t.TempDir(), "prov", tfp) + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test = ["a", "a"] +}`) + + plan, err := tfdriver.Plan(t) + require.NoError(t, err) + err = tfdriver.Apply(t, plan) + require.NoError(t, err) + + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test = ["b", "b", "a", "a", "c"] +}`) + + plan, err = tfdriver.Plan(t) + require.NoError(t, err) + + autogold.Expect(` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # prov_test.mainRes will be updated in-place + ~ resource "prov_test" "mainRes" { + id = "newid" + ~ test = [ + + "b", + + "c", + # (1 unchanged element hidden) + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +`).Equal(t, plan.StdOut) + }) +} + +func TestDetailedDiffSetNestedAttributeUpdated(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{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nested": { + Type: schema.TypeString, + Optional: true, + }, + "nested2": { + Type: schema.TypeString, + Optional: true, + }, + "nested3": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + } + + tfp := &schema.Provider{ResourcesMap: resMap} + + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s` + + t.Run("pulumi", func(t *testing.T) { + props1 := []map[string]string{ + {"nested": "b", "nested2": "b", "nested3": "b"}, + {"nested": "a", "nested2": "a", "nested3": "a"}, + {"nested": "c", "nested2": "c", "nested3": "c"}, + } + props2 := []map[string]string{ + {"nested": "b", "nested2": "b", "nested3": "b"}, + {"nested": "d", "nested2": "a", "nested3": "a"}, + {"nested": "c", "nested2": "c", "nested3": "c"}, + } + + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + + pt := pulcheck.PulCheck(t, bridgedProvider, fmt.Sprintf(program, string(props1JSON))) + pt.Up(t) + + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + + pt.WritePulumiYaml(t, fmt.Sprintf(program, string(props2JSON))) + + res := pt.Preview(t, optpreview.Diff()) + + 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] + ~ tests: [ + - [0]: { + - nested : "a" + - nested2: "a" + - nested3: "a" + } + + [1]: { + + nested : "d" + + nested2 : "a" + + nested3 : "a" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`).Equal(t, res.StdOut) + }) + + t.Run("terraform", func(t *testing.T) { + tfdriver := tfcheck.NewTfDriver(t, t.TempDir(), "prov", tfp) + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "b" + nested2 = "b" + nested3 = "b" + } + test { + nested = "a" + nested2 = "a" + nested3 = "a" + } + test { + nested = "c" + nested2 = "c" + nested3 = "c" + } +}`) + + plan, err := tfdriver.Plan(t) + require.NoError(t, err) + err = tfdriver.Apply(t, plan) + require.NoError(t, err) + + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "b" + nested2 = "b" + nested3 = "b" + } + test { + nested = "d" + nested2 = "a" + nested3 = "a" + } + test { + nested = "c" + nested2 = "c" + nested3 = "c" + } +}`) + + plan, err = tfdriver.Plan(t) + require.NoError(t, err) + + autogold.Expect(` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # prov_test.mainRes will be updated in-place + ~ resource "prov_test" "mainRes" { + id = "newid" + + - test { + - nested = "a" -> null + - nested2 = "a" -> null + - nested3 = "a" -> null + } + + test { + + nested = "d" + + nested2 = "a" + + nested3 = "a" + } + + # (2 unchanged blocks hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +`).Equal(t, plan.StdOut) + }) +} + +func TestDetailedDiffSetComputedNestedAttribute(t *testing.T) { + // TODO: Remove this once accurate bridge previews are rolled out + t.Setenv("PULUMI_TF_BRIDGE_ACCURATE_BRIDGE_PREVIEW", "true") + + resCount := 0 + setComputedProp := func(t *testing.T, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + testSet := d.Get("test").(*schema.Set) + testVals := testSet.List() + newTestVals := make([]interface{}, len(testVals)) + for i, v := range testVals { + val := v.(map[string]interface{}) + if val["computed"] == nil || val["computed"] == "" { + val["computed"] = fmt.Sprint(resCount) + resCount++ + } + newTestVals[i] = val + } + + err := d.Set("test", schema.NewSet(testSet.F, newTestVals)) + require.NoError(t, err) + return nil + } + + resMap := map[string]*schema.Resource{ + "prov_test": { + Schema: map[string]*schema.Schema{ + "test": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nested": { + Type: schema.TypeString, + Optional: true, + }, + "computed": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + return setComputedProp(t, d, i) + }, + UpdateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + return setComputedProp(t, d, i) + }, + }, + } + + tfp := &schema.Provider{ResourcesMap: resMap} + bridgedProvider := pulcheck.BridgedProvider(t, "prov", tfp) + + program := ` +name: test +runtime: yaml +resources: + mainRes: + type: prov:index:Test + properties: + tests: %s` + + t.Run("pulumi", func(t *testing.T) { + props1 := []map[string]string{ + {"nested": "a", "computed": "b"}, + } + props1JSON, err := json.Marshal(props1) + require.NoError(t, err) + + pt := pulcheck.PulCheck(t, bridgedProvider, fmt.Sprintf(program, string(props1JSON))) + pt.Up(t) + + props2 := []map[string]string{ + {"nested": "a"}, + {"nested": "a", "computed": "b"}, + } + props2JSON, err := json.Marshal(props2) + require.NoError(t, err) + + pt.WritePulumiYaml(t, fmt.Sprintf(program, string(props2JSON))) + res := pt.Preview(t, optpreview.Diff()) + + // TODO:[pulumi/pulumi-terraform-bridge#2200] The diff here is wrong + 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=id] + [urn=urn:pulumi:test::test::prov:index/test:Test::mainRes] + ~ tests: [ + + [0]: { + + nested : "a" + } + ] +Resources: + ~ 1 to update + 1 unchanged +`).Equal(t, res.StdOut) + }) + + t.Run("terraform", func(t *testing.T) { + resCount = 0 + tfdriver := tfcheck.NewTfDriver(t, t.TempDir(), "prov", tfp) + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "a" + computed = "b" + } +}`) + + plan, err := tfdriver.Plan(t) + require.NoError(t, err) + err = tfdriver.Apply(t, plan) + require.NoError(t, err) + + tfdriver.Write(t, ` +resource "prov_test" "mainRes" { + test { + nested = "a" + computed = "b" + } + test { + nested = "a" + } +}`) + plan, err = tfdriver.Plan(t) + require.NoError(t, err) + + autogold.Expect(` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # prov_test.mainRes will be updated in-place + ~ resource "prov_test" "mainRes" { + id = "id" + + + test { + + computed = (known after apply) + + nested = "a" + } + + # (1 unchanged block hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +`).Equal(t, plan.StdOut) + }) +} diff --git a/pkg/tests/tfcheck/tfcheck.go b/pkg/tests/tfcheck/tfcheck.go index 08a93049f..bc4dd8104 100644 --- a/pkg/tests/tfcheck/tfcheck.go +++ b/pkg/tests/tfcheck/tfcheck.go @@ -32,6 +32,7 @@ type TfDriver struct { } type TfPlan struct { + StdOut string PlanFile string RawPlan any } @@ -123,13 +124,15 @@ func (d *TfDriver) Plan(t pulcheck.T) (*TfPlan, error) { planFile := filepath.Join(d.cwd, "test.tfplan") env := []string{d.formatReattachEnvVar()} tfCmd := getTFCommand() - _, err := execCmd(t, d.cwd, env, tfCmd, "plan", "-refresh=false", "-out", planFile) + cm, err := execCmd(t, d.cwd, env, tfCmd, "plan", "-refresh=false", "-out", planFile, "-no-color") if err != nil { return nil, err } + planStdout := cm.Stdout.(*bytes.Buffer).String() + planStdout = strings.Split(planStdout, "───")[0] // trim unstable output about the plan file cmd, err := execCmd(t, d.cwd, env, tfCmd, "show", "-json", planFile) require.NoError(t, err) - tp := TfPlan{PlanFile: planFile} + tp := TfPlan{PlanFile: planFile, StdOut: planStdout} err = json.Unmarshal(cmd.Stdout.(*bytes.Buffer).Bytes(), &tp.RawPlan) require.NoErrorf(t, err, "failed to unmarshal terraform plan") return &tp, nil diff --git a/pkg/tfbridge/detailed_diff.go b/pkg/tfbridge/detailed_diff.go index 0865f9114..a5e5b3038 100644 --- a/pkg/tfbridge/detailed_diff.go +++ b/pkg/tfbridge/detailed_diff.go @@ -5,6 +5,7 @@ import ( "context" "slices" + "github.com/golang/glog" "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" @@ -124,6 +125,8 @@ type detailedDiffKey string type detailedDiffer struct { tfs shim.SchemaMap ps map[string]*SchemaInfo + // These are used to convert set indices back to something the engine can reference. + newInputs resource.PropertyMap } func (differ detailedDiffer) propertyPathToSchemaPath(path propertyPath) walk.SchemaPath { @@ -149,6 +152,37 @@ func (differ detailedDiffer) getEffectiveType(path walk.SchemaPath) shim.ValueTy return tfs.Type() } +type hashIndexMap map[int]int + +func (differ detailedDiffer) calculateSetHashIndexMap(path propertyPath, listVal resource.PropertyValue) hashIndexMap { + identities := make(hashIndexMap) + + tfs, ps, err := lookupSchemas(path, differ.tfs, differ.ps) + if err != nil { + return nil + } + + convertedVal, err := makeSingleTerraformInput(context.Background(), path.String(), listVal, tfs, ps) + if err != nil { + return nil + } + + if convertedVal == nil { + return nil + } + + convertedListVal, ok := convertedVal.([]interface{}) + contract.Assertf(ok, "converted value should be a list") + + // Calculate the identity of each element + for i, newElem := range convertedListVal { + + hash := tfs.SetHash(newElem) + identities[hash] = i + } + return identities +} + // 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( @@ -220,8 +254,7 @@ func (differ detailedDiffer) makePropDiff( 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) + return differ.makeSetDiff(path, old, new) case shim.TypeMap: // Note that TF objects are represented as maps when returned by LookupSchemas return differ.makeMapDiff(path, old, new) @@ -259,6 +292,78 @@ func (differ detailedDiffer) makeListDiff( return diff } +type setChangeIndex struct { + engineIndex int + newStateIndex int + oldChanged bool + newChanged bool +} + +func (differ detailedDiffer) makeSetDiff( + path propertyPath, old, new resource.PropertyValue, +) map[detailedDiffKey]*pulumirpc.PropertyDiff { + diff := make(map[detailedDiffKey]*pulumirpc.PropertyDiff) + oldList := old.ArrayValue() + newList := new.ArrayValue() + newInputsList := []resource.PropertyValue{} + + newInputs, newInputsOk := path.GetFromMap(differ.newInputs) + if newInputsOk && isPresent(newInputs) && newInputs.IsArray() { + newInputsList = newInputs.ArrayValue() + } + + oldIdentities := differ.calculateSetHashIndexMap(path, resource.NewArrayProperty(oldList)) + newIdentities := differ.calculateSetHashIndexMap(path, resource.NewArrayProperty(newList)) + inputIdentities := hashIndexMap{} + + if !schemaContainsComputed(path, differ.tfs, differ.ps) { + // The inputs are only safe to hash if the schema has no computed properties + inputIdentities = differ.calculateSetHashIndexMap(path, resource.NewArrayProperty(newInputsList)) + } + + // The old indices and new inputs are the indices the engine can reference + // The new state indices need to be translated to new input indices when presenting the diff + setIndices := make(map[int]setChangeIndex) + for hash, oldIndex := range oldIdentities { + if _, newOk := newIdentities[hash]; !newOk { + setIndices[oldIndex] = setChangeIndex{engineIndex: oldIndex, oldChanged: true, newStateIndex: -1, newChanged: false} + } + } + for hash, newIndex := range newIdentities { + if _, oldOk := oldIdentities[hash]; !oldOk { + inputIndex := inputIdentities[hash] + if inputIndex == -1 { + glog.Warningln( + "Element at path %s in new state not found in inputs, the displayed diff might be inaccurate", + path.String()) + inputIndex = newIndex + } + _, oldChanged := setIndices[inputIndex] + setIndices[inputIndex] = setChangeIndex{ + engineIndex: inputIndex, oldChanged: oldChanged, newStateIndex: newIndex, newChanged: true, + } + } + } + + for index, setChange := range setIndices { + oldEl := resource.NewNullProperty() + if setChange.oldChanged { + oldEl = oldList[setChange.engineIndex] + } + newEl := resource.NewNullProperty() + if setChange.newChanged { + contract.Assertf(setChange.newStateIndex != -1, "new state index should be set") + newEl = newList[setChange.newStateIndex] + } + d := differ.makePropDiff(path.Index(index), oldEl, newEl) + for subKey, subDiff := range d { + diff[subKey] = subDiff + } + } + + return diff +} + func (differ detailedDiffer) makeMapDiff( path propertyPath, old, new resource.PropertyValue, ) map[detailedDiffKey]*pulumirpc.PropertyDiff { @@ -314,6 +419,7 @@ func makeDetailedDiffV2( diff shim.InstanceDiff, assets AssetTable, supportsSecrets bool, + newInputs resource.PropertyMap, ) (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. @@ -335,6 +441,6 @@ func makeDetailedDiffV2( return nil, err } - differ := detailedDiffer{tfs: tfs, ps: ps} + differ := detailedDiffer{tfs: tfs, ps: ps, newInputs: newInputs} return differ.makeDetailedDiffPropertyMap(priorProps, props), nil } diff --git a/pkg/tfbridge/detailed_diff_test.go b/pkg/tfbridge/detailed_diff_test.go index 2ab8306fe..0c7464dc3 100644 --- a/pkg/tfbridge/detailed_diff_test.go +++ b/pkg/tfbridge/detailed_diff_test.go @@ -1,6 +1,7 @@ package tfbridge import ( + "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -288,7 +289,7 @@ func runDetailedDiffTest( expected map[string]*pulumirpc.PropertyDiff, ) { t.Helper() - differ := detailedDiffer{tfs: tfs, ps: ps} + differ := detailedDiffer{tfs: tfs, ps: ps, newInputs: new} actual := differ.makeDetailedDiffPropertyMap(old, new) require.Equal(t, expected, actual) @@ -957,7 +958,7 @@ func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) { value1: []interface{}{"val1"}, value2: []interface{}{"val2"}, computedCollection: ComputedVal, - computedElem: []interface{}{ComputedVal}, + computedElem: nil, }, { name: "map", @@ -998,6 +999,7 @@ func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) { "prop": tt.computedCollection, }, ) + propertyMapComputedElem := resource.NewPropertyMapFromMap( map[string]interface{}{ "prop": tt.computedElem, @@ -1033,23 +1035,26 @@ func TestDetailedDiffTFForceNewAttributeCollection(t *testing.T) { }) }) - 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}, + if tt.computedElem != nil { + + 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 elem", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapEmpty, propertyMapComputedElem, tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "prop": {Kind: pulumirpc.PropertyDiff_ADD_REPLACE}, + }) + }) + } }) } } @@ -1747,3 +1752,573 @@ func TestDetailedDiffPulumiSchemaOverride(t *testing.T) { }) }) } + +func TestDetailedDiffSetAttribute(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) + + propertyMapElems := func(elems ...interface{}) resource.PropertyMap { + return resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": elems, + }, + ) + } + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapElems("val1"), propertyMapElems("val1"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems("val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + propertyMapElems("val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems(), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("added end", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapElems("val1", "val2"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("same element updated", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val4", "val3"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("shuffled", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled with duplicates", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("shuffled added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("shuffled added end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val2", "val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_ADD}, + }) + }) + + t.Run("shuffled removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("shuffled removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("shuffled removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }) + }) + + t.Run("computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("nil to computed", func(t *testing.T) { + runDetailedDiffTest(t, + resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD}, + }, + ) + }) + + t.Run("empty to computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("two added, two removed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val3", "val4"), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + + t.Run("two added, two removed, shuffled", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("stable1", "stable2", "val1", "val2"), + propertyMapElems("val4", "val3", "stable1", "stable2"), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo[1]": {Kind: pulumirpc.PropertyDiff_ADD}, + "foo[2]": {Kind: pulumirpc.PropertyDiff_DELETE}, + "foo[3]": {Kind: pulumirpc.PropertyDiff_DELETE}, + }, + ) + }) +} + +func TestDetailedDiffSetBlock(t *testing.T) { + propertyMapElems := func(elems ...string) resource.PropertyMap { + var elemMaps []map[string]interface{} + for _, elem := range elems { + elemMaps = append(elemMaps, map[string]interface{}{"bar": elem}) + } + return resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": elemMaps, + }, + ) + } + + for _, forceNew := range []bool{false, true} { + sdkv2Schema := map[string]*schema.Schema{ + "foo": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bar": { + Type: schema.TypeString, + Optional: true, + ForceNew: forceNew, + }, + }, + }, + }, + } + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(sdkv2Schema) + t.Run(fmt.Sprintf("forceNew=%v", forceNew), func(t *testing.T) { + update := pulumirpc.PropertyDiff_UPDATE + add := pulumirpc.PropertyDiff_ADD + delete := pulumirpc.PropertyDiff_DELETE + if forceNew { + update = pulumirpc.PropertyDiff_UPDATE_REPLACE + add = pulumirpc.PropertyDiff_ADD_REPLACE + delete = pulumirpc.PropertyDiff_DELETE_REPLACE + } + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, propertyMapElems("val1"), propertyMapElems("val1"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("changed non-empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems("val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0].bar": {Kind: update}, + }, + ) + }) + + t.Run("changed from empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + propertyMapElems("val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: add}, + }, + ) + }) + + t.Run("changed to empty", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + propertyMapElems(), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: delete}, + }, + ) + }) + + t.Run("removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: delete}, + }, + ) + }) + + t.Run("removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: delete}, + }, + ) + }) + + t.Run("removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: delete}, + }, + ) + }) + + t.Run("added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: add}, + }, + ) + }) + + t.Run("added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: add}, + }, + ) + }) + + t.Run("added end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val1", "val2", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: add}, + }, + ) + }) + + t.Run("same element updated", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val1", "val4", "val3"), tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo[1].bar": {Kind: update}, + }, + ) + }) + + t.Run("shuffled", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled with duplicates", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2", "val1", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) + + t.Run("shuffled added front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val2", "val3"), + propertyMapElems("val1", "val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: add}, + }, + ) + }) + + t.Run("shuffled added middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val3"), + propertyMapElems("val3", "val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: add}, + }, + ) + }) + + t.Run("shuffled added end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2"), + propertyMapElems("val2", "val1", "val3"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: add}, + }, + ) + }) + + t.Run("shuffled removed front", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val2"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[0]": {Kind: delete}, + }, + ) + }) + + t.Run("shuffled removed middle", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val3", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[1]": {Kind: delete}, + }, + ) + }) + + t.Run("shuffled removed end", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1", "val2", "val3"), + propertyMapElems("val2", "val1"), tfs, ps, map[string]*pulumirpc.PropertyDiff{ + "foo[2]": {Kind: delete}, + }, + ) + }) + + t.Run("computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems("val1"), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: update}, + }, + ) + }) + + t.Run("nil to computed", func(t *testing.T) { + runDetailedDiffTest(t, + resource.NewPropertyMapFromMap( + map[string]interface{}{}, + ), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_ADD}, + }, + ) + }) + + t.Run("empty to computed", func(t *testing.T) { + runDetailedDiffTest(t, + propertyMapElems(), + resource.NewPropertyMapFromMap( + map[string]interface{}{ + "foo": computedValue, + }, + ), + tfs, ps, + map[string]*pulumirpc.PropertyDiff{ + "foo": {Kind: pulumirpc.PropertyDiff_UPDATE}, + }, + ) + }) + }) + } +} + +func TestDetailedDiffSetBlockNestedMaxItemsOne(t *testing.T) { + customResponseSchema := func() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_response_body_key": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + } + } + blockConfigSchema := func() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_response": customResponseSchema(), + }, + }, + } + } + ruleElement := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "block": blockConfigSchema(), + }, + }, + }, + }, + } + + schMap := map[string]*schema.Schema{ + "rule": { + Type: schema.TypeSet, + Optional: true, + Elem: ruleElement, + }, + } + + ps, tfs := map[string]*info.Schema{}, shimv2.NewSchemaMap(schMap) + + t.Run("unchanged", func(t *testing.T) { + runDetailedDiffTest(t, resource.NewPropertyMapFromMap(map[string]interface{}{ + "rule": []map[string]interface{}{ + { + "action": map[string]interface{}{ + "block": map[string]interface{}{ + "custom_response": map[string]interface{}{ + "custom_response_body_key": "val1", + }, + }, + }, + }, + }, + }), resource.NewPropertyMapFromMap(map[string]interface{}{ + "rule": []map[string]interface{}{ + { + "action": map[string]interface{}{ + "block": map[string]interface{}{ + "custom_response": map[string]interface{}{ + "custom_response_body_key": "val1", + }, + }, + }, + }, + }, + }), tfs, ps, map[string]*pulumirpc.PropertyDiff{}) + }) +} diff --git a/pkg/tfbridge/property_path.go b/pkg/tfbridge/property_path.go index 8ffad9eb1..83dddada4 100644 --- a/pkg/tfbridge/property_path.go +++ b/pkg/tfbridge/property_path.go @@ -2,11 +2,37 @@ package tfbridge import ( "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "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" ) +// a variant of PropertyPath.Get which works on PropertyMaps +func getPathFromPropertyMap( + path resource.PropertyPath, propertyMap resource.PropertyMap, +) (resource.PropertyValue, bool) { + if len(path) == 0 { + return resource.NewNullProperty(), false + } + + rootKeyStr, ok := path[0].(string) + contract.Assertf(ok && rootKeyStr != "", "root key must be a non-empty string") + rootKey := resource.PropertyKey(rootKeyStr) + restPath := path[1:] + + if len(restPath) == 0 { + return propertyMap[rootKey], true + } + + if !propertyMap.HasValue(rootKey) { + return resource.NewNullProperty(), false + } + + return restPath.Get(propertyMap[rootKey]) +} + type propertyPath resource.PropertyPath func isForceNew(tfs shim.Schema, ps *SchemaInfo) bool { @@ -43,6 +69,10 @@ func (k propertyPath) IsReservedKey() bool { return leaf == "__meta" || leaf == "__defaults" } +func (k propertyPath) GetFromMap(v resource.PropertyMap) (resource.PropertyValue, bool) { + return getPathFromPropertyMap(resource.PropertyPath(k), v) +} + func lookupSchemas( path propertyPath, tfs shim.SchemaMap, ps map[string]*info.Schema, ) (shim.Schema, *info.Schema, error) { @@ -125,3 +155,23 @@ func willTriggerReplacementRecursive( return replacement } + +func schemaContainsComputed( + path propertyPath, rootTFSchema shim.SchemaMap, rootPulumiSchema map[string]*info.Schema, +) bool { + computed := false + visitor := func(path walk.SchemaPath, tfs shim.Schema) { + if tfs.Computed() { + computed = true + } + } + + tfs, _, err := lookupSchemas(path, rootTFSchema, rootPulumiSchema) + if err != nil { + return false + } + + walk.VisitSchema(tfs, visitor) + + return computed +} diff --git a/pkg/tfbridge/provider.go b/pkg/tfbridge/provider.go index 5ed4eecef..a9d50155f 100644 --- a/pkg/tfbridge/provider.go +++ b/pkg/tfbridge/provider.go @@ -1169,7 +1169,8 @@ func (p *Provider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pulum changes = pulumirpc.DiffResponse_DIFF_SOME } - detailedDiff, err = makeDetailedDiffV2(ctx, schema, fields, res.TF, p.tf, state, diff, assets, p.supportsSecrets) + detailedDiff, err = makeDetailedDiffV2( + ctx, schema, fields, res.TF, p.tf, state, diff, assets, p.supportsSecrets, news) if err != nil { return nil, err } diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go index 4018c0b12..bde04525f 100644 --- a/pkg/tfbridge/schema.go +++ b/pkg/tfbridge/schema.go @@ -341,6 +341,24 @@ func MakeTerraformInputs( return makeTerraformInputsWithOptions(ctx, instance, config, olds, news, tfs, ps, makeTerraformInputsOptions{}) } +// Converts a single Pulumi property value into a plain go value suitable for use by Terraform. +// This does not apply any defaults or other transformations. +func makeSingleTerraformInput( + ctx context.Context, name string, val resource.PropertyValue, tfs shim.Schema, ps *SchemaInfo, +) (interface{}, error) { + cctx := &conversionContext{ + Ctx: ctx, + ComputeDefaultOptions: ComputeDefaultOptions{}, + ProviderConfig: nil, + ApplyDefaults: false, + ApplyTFDefaults: false, + Assets: AssetTable{}, + UnknownCollectionsSupported: false, + } + + return cctx.makeTerraformInput(name, resource.NewNullProperty(), val, tfs, ps) +} + // makeTerraformInput takes a single property plus custom schema info and does whatever is necessary // to prepare it for use by Terraform. Note that this function may have side effects, for instance // if it is necessary to spill an asset to disk in order to create a name out of it. Please take diff --git a/pkg/tfbridge/schema_test.go b/pkg/tfbridge/schema_test.go index 9cc780c2f..b79f09705 100644 --- a/pkg/tfbridge/schema_test.go +++ b/pkg/tfbridge/schema_test.go @@ -3759,3 +3759,127 @@ func TestExtractInputsFromOutputsSdkv2(t *testing.T) { } } + +func TestMakeSingleTerraformInput(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + prop resource.PropertyValue + schema *schemav2.Schema + expected interface{} + } + + testCases := []testCase{ + { + name: "bool", + prop: resource.NewBoolProperty(true), + schema: &schemav2.Schema{ + Type: schemav2.TypeBool, + Optional: true, + }, + expected: true, + }, + { + name: "number", + prop: resource.NewNumberProperty(42), + schema: &schemav2.Schema{ + Type: schemav2.TypeInt, + Optional: true, + }, + expected: 42, + }, + { + name: "string", + prop: resource.NewStringProperty("foo"), + schema: &schemav2.Schema{ + Type: schemav2.TypeString, + Optional: true, + }, + expected: "foo", + }, + { + name: "array", + prop: resource.NewArrayProperty([]resource.PropertyValue{ + resource.NewStringProperty("foo"), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeList, + Optional: true, + Elem: &schema.Schema{Type: shim.TypeString}, + }, + expected: []interface{}{"foo"}, + }, + { + name: "map", + prop: resource.NewObjectProperty(resource.PropertyMap{ + "foo": resource.NewStringProperty("bar"), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: shim.TypeString}, + }, + expected: map[string]interface{}{"foo": "bar"}, + }, + { + name: "object", + prop: resource.NewObjectProperty(resource.PropertyMap{ + "foo": resource.NewStringProperty("bar"), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: schema.SchemaMap{ + "foo": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(), + }, + }, + }, + expected: []interface{}{map[string]interface{}{"foo": "bar"}}, + }, + { + name: "nested object", + prop: resource.NewObjectProperty(resource.PropertyMap{ + "foo": resource.NewObjectProperty(resource.PropertyMap{ + "bar": resource.NewStringProperty("baz"), + }), + }), + schema: &schemav2.Schema{ + Type: schemav2.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schemav2.Resource{ + Schema: map[string]*schemav2.Schema{ + "foo": { + Type: schemav2.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schemav2.Resource{ + Schema: map[string]*schemav2.Schema{ + "bar": {Type: schemav2.TypeString, Optional: true}, + }, + }, + }, + }, + }, + }, + expected: []interface{}{map[string]interface{}{ + "foo": []interface{}{map[string]interface{}{ + "bar": "baz", + }}, + }}, + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + result, err := makeSingleTerraformInput(context.Background(), "name", tc.prop, shimv2.NewSchema(tc.schema), nil) + require.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/x/muxer/go.mod b/x/muxer/go.mod index c4b30be0b..2fc679397 100644 --- a/x/muxer/go.mod +++ b/x/muxer/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.1 replace github.com/pulumi/pulumi-terraform-bridge/v3 => ../../ require ( - github.com/pulumi/providertest v0.1.2 + github.com/pulumi/providertest v0.1.3 github.com/pulumi/pulumi-terraform-bridge/v3 v3.0.0-00010101000000-000000000000 github.com/stretchr/testify v1.9.0 google.golang.org/protobuf v1.34.2 diff --git a/x/muxer/go.sum b/x/muxer/go.sum index 8d1aed58a..896afe340 100644 --- a/x/muxer/go.sum +++ b/x/muxer/go.sum @@ -154,8 +154,8 @@ github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435 github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= -github.com/pulumi/providertest v0.1.2 h1:9pJS9MeNkMyGwyNeHmvh8QqLgJy39Nk2/ym5u7r13ng= -github.com/pulumi/providertest v0.1.2/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= +github.com/pulumi/providertest v0.1.3 h1:GpNKRy/haNjRHiUA9bi4diU4Op2zf3axYXbga5AepHg= +github.com/pulumi/providertest v0.1.3/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= github.com/pulumi/pulumi/pkg/v3 v3.136.1 h1:zA8aJZ7qI0QgZkBKjjQaYHEcigK6pZfrbfG38imXzWo= github.com/pulumi/pulumi/pkg/v3 v3.136.1/go.mod h1:Iz8QIs07AbEdrO52hEIEM5C4VBDUYFH2NdM9u2xxBxY= github.com/pulumi/pulumi/sdk/v3 v3.136.1 h1:VJWTgdBrLvvzIkMbGq/epNEfT65P9gTvw14UF/I7hTI=