package lifecycletest import ( "testing" "github.com/blang/semver" "github.com/stretchr/testify/assert" . "github.com/pulumi/pulumi/pkg/v3/engine" //nolint:revive "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" ) // TestResourceReferences tests that resource references can be marshaled between the engine, language host, // resource providers, and statefile if each entity supports resource references. func TestResourceReferences(t *testing.T) { t.Parallel() var urnA resource.URN var urnB resource.URN var idB resource.ID loaders := []*deploytest.ProviderLoader{ deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { v := &deploytest.Provider{ CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, preview bool, ) (resource.ID, resource.PropertyMap, resource.Status, error) { id := "created-id" if preview { id = "" } if urn.Name() == "resC" { assert.True(t, news.DeepEquals(resource.PropertyMap{ "resA": resource.MakeComponentResourceReference(urnA, ""), "resB": resource.MakeCustomResourceReference(urnB, idB, ""), })) } return resource.ID(id), news, resource.StatusOK, nil }, ReadF: func(urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, ) (plugin.ReadResult, resource.Status, error) { return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil }, } return v, nil }), } programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { var err error respA, err := monitor.RegisterResource("component", "resA", false) assert.NoError(t, err) urnA = respA.URN err = monitor.RegisterResourceOutputs(urnA, resource.PropertyMap{}) assert.NoError(t, err) respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true) assert.NoError(t, err) urnB, idB = respB.URN, respB.ID resp, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{ Inputs: resource.PropertyMap{ "resA": resource.MakeComponentResourceReference(urnA, ""), "resB": resource.MakeCustomResourceReference(urnB, idB, ""), }, }) assert.NoError(t, err) assert.True(t, resp.Outputs.DeepEquals(resource.PropertyMap{ "resA": resource.MakeComponentResourceReference(urnA, ""), "resB": resource.MakeCustomResourceReference(urnB, idB, ""), })) return nil }) hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...) p := &TestPlan{ // Skip display tests because different ordering makes the colouring different. Options: TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true}, Steps: MakeBasicLifecycleSteps(t, 4), } p.Run(t, nil) } // TestResourceReferences_DownlevelSDK tests that resource references are properly marshaled as URNs (for references to // component resources) or IDs (for references to custom resources) if the SDK does not support resource references. func TestResourceReferences_DownlevelSDK(t *testing.T) { t.Parallel() var urnA resource.URN var urnB resource.URN var idB resource.ID loaders := []*deploytest.ProviderLoader{ deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { v := &deploytest.Provider{ CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, preview bool, ) (resource.ID, resource.PropertyMap, resource.Status, error) { id := "created-id" if preview { id = "" } state := resource.PropertyMap{} if urn.Name() == "resC" { state = resource.PropertyMap{ "resA": resource.MakeComponentResourceReference(urnA, ""), "resB": resource.MakeCustomResourceReference(urnB, idB, ""), } } return resource.ID(id), state, resource.StatusOK, nil }, ReadF: func(urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, ) (plugin.ReadResult, resource.Status, error) { return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil }, } return v, nil }), } opts := deploytest.ResourceOptions{DisableResourceReferences: true} programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { var err error respA, err := monitor.RegisterResource("component", "resA", false, opts) assert.NoError(t, err) urnA = respA.URN err = monitor.RegisterResourceOutputs(urnA, resource.PropertyMap{}) assert.NoError(t, err) respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, opts) assert.NoError(t, err) urnB, idB = respB.URN, respB.ID respC, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, opts) assert.NoError(t, err) assert.Equal(t, resource.NewStringProperty(string(urnA)), respC.Outputs["resA"]) if idB != "" { assert.Equal(t, resource.NewStringProperty(string(idB)), respC.Outputs["resB"]) } else { assert.True(t, respC.Outputs["resB"].IsComputed()) } return nil }) hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...) p := &TestPlan{ // Skip display tests because different ordering makes the colouring different. Options: TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true}, Steps: MakeBasicLifecycleSteps(t, 4), } p.Run(t, nil) } // TestResourceReferences_DownlevelEngine tests an SDK that supports resource references communicating with an engine // that does not. func TestResourceReferences_DownlevelEngine(t *testing.T) { t.Parallel() var urnA resource.URN var refB resource.PropertyValue loaders := []*deploytest.ProviderLoader{ deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { v := &deploytest.Provider{ CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, preview bool, ) (resource.ID, resource.PropertyMap, resource.Status, error) { id := "created-id" if preview { id = "" } // If we have resource references here, the engine has not properly disabled them. if urn.Name() == "resC" { assert.Equal(t, resource.NewStringProperty(string(urnA)), news["resA"]) assert.Equal(t, refB.ResourceReferenceValue().ID, news["resB"]) } return resource.ID(id), news, resource.StatusOK, nil }, ReadF: func(urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, ) (plugin.ReadResult, resource.Status, error) { return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil }, } return v, nil }), } programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { var err error respA, err := monitor.RegisterResource("component", "resA", false) assert.NoError(t, err) urnA = respA.URN err = monitor.RegisterResourceOutputs(urnA, resource.PropertyMap{}) assert.NoError(t, err) respB, err := monitor.RegisterResource("pkgA:m:typA", "resB", true) assert.NoError(t, err) refB = resource.MakeCustomResourceReference(respB.URN, respB.ID, "") resp, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{ Inputs: resource.PropertyMap{ "resA": resource.MakeComponentResourceReference(urnA, ""), "resB": refB, }, }) assert.NoError(t, err) assert.Equal(t, resource.NewStringProperty(string(urnA)), resp.Outputs["resA"]) if refB.ResourceReferenceValue().ID.IsComputed() { assert.True(t, resp.Outputs["resB"].IsComputed()) } else { assert.True(t, refB.ResourceReferenceValue().ID.DeepEquals(resp.Outputs["resB"])) } return nil }) hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...) p := &TestPlan{ // Skip display tests because different ordering makes the colouring different. Options: TestUpdateOptions{ T: t, HostF: hostF, UpdateOptions: UpdateOptions{DisableResourceReferences: true}, SkipDisplayTests: true, }, Steps: MakeBasicLifecycleSteps(t, 4), } p.Run(t, nil) } // TestResourceReferences_GetResource tests that invoking the built-in 'pulumi:pulumi:getResource' function // returns resource references for any resource reference in a resource's state. func TestResourceReferences_GetResource(t *testing.T) { t.Parallel() loaders := []*deploytest.ProviderLoader{ deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { v := &deploytest.Provider{ CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, preview bool, ) (resource.ID, resource.PropertyMap, resource.Status, error) { id := "created-id" if preview { id = "" } return resource.ID(id), news, resource.StatusOK, nil }, ReadF: func(urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, ) (plugin.ReadResult, resource.Status, error) { return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil }, } return v, nil }), } programF := deploytest.NewLanguageRuntimeF(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { childResp, err := monitor.RegisterResource("pkgA:m:typChild", "resChild", true) assert.NoError(t, err) refChild := resource.MakeCustomResourceReference(childResp.URN, childResp.ID, "") resp, err := monitor.RegisterResource("pkgA:m:typContainer", "resContainer", true, deploytest.ResourceOptions{ Inputs: resource.PropertyMap{ "child": refChild, }, }) assert.NoError(t, err) // Expect the `child` property from `resContainer`'s state to come back from 'pulumi:pulumi:getResource' // as a resource reference. result, failures, err := monitor.Invoke("pulumi:pulumi:getResource", resource.PropertyMap{ "urn": resource.NewStringProperty(string(resp.URN)), }, "", "") assert.NoError(t, err) assert.Empty(t, failures) assert.Equal(t, resource.NewStringProperty(string(resp.URN)), result["urn"]) assert.Equal(t, resource.NewStringProperty(string(resp.ID)), result["id"]) state := result["state"].ObjectValue() assert.Equal(t, refChild, state["child"]) return nil }) hostF := deploytest.NewPluginHostF(nil, nil, programF, loaders...) p := &TestPlan{ // Skip display tests because different ordering makes the colouring different. Options: TestUpdateOptions{T: t, HostF: hostF, SkipDisplayTests: true}, Steps: MakeBasicLifecycleSteps(t, 4), } p.Run(t, nil) }