pulumi/pkg/engine/lifecycletest/resource_reference_test.go

312 lines
10 KiB
Go

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)
}