pulumi/pkg/engine/lifeycletest/target_test.go

583 lines
17 KiB
Go

package lifecycletest
import (
"testing"
"github.com/blang/semver"
combinations "github.com/mxschmitt/golang-combinations"
"github.com/stretchr/testify/assert"
. "github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"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"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
func TestDestroyTarget(t *testing.T) {
// Try refreshing a stack with combinations of the above resources as target to destroy.
subsets := combinations.All(complexTestDependencyGraphNames)
for _, subset := range subsets {
// limit to up to 3 resources to destroy. This keeps the test running time under
// control as it only generates a few hundred combinations instead of several thousand.
if len(subset) <= 3 {
destroySpecificTargets(t, subset, true, /*targetDependents*/
func(urns []resource.URN, deleted map[resource.URN]bool) {})
}
}
destroySpecificTargets(
t, []string{"A"}, true, /*targetDependents*/
func(urns []resource.URN, deleted map[resource.URN]bool) {
// when deleting 'A' we expect A, B, C, E, F, and K to be deleted
names := complexTestDependencyGraphNames
assert.Equal(t, map[resource.URN]bool{
pickURN(t, urns, names, "A"): true,
pickURN(t, urns, names, "B"): true,
pickURN(t, urns, names, "C"): true,
pickURN(t, urns, names, "E"): true,
pickURN(t, urns, names, "F"): true,
pickURN(t, urns, names, "K"): true,
}, deleted)
})
destroySpecificTargets(
t, []string{"A"}, false, /*targetDependents*/
func(urns []resource.URN, deleted map[resource.URN]bool) {})
}
func destroySpecificTargets(
t *testing.T, targets []string, targetDependents bool,
validate func(urns []resource.URN, deleted map[resource.URN]bool)) {
// A
// _________|_________
// B C D
// ___|___ ___|___
// E F G H I J
// |__|
// K L
p := &TestPlan{}
urns, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{
ReplaceKeys: []resource.PropertyKey{"A"},
DeleteBeforeReplace: true,
}, nil
}
return plugin.DiffResult{}, nil
},
DiffF: func(urn resource.URN, id resource.ID,
olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) {
if !olds["A"].DeepEquals(news["A"]) {
return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil
}
return plugin.DiffResult{}, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Options.TargetDependents = targetDependents
destroyTargets := []resource.URN{}
for _, target := range targets {
destroyTargets = append(destroyTargets, pickURN(t, urns, complexTestDependencyGraphNames, target))
}
p.Options.DestroyTargets = destroyTargets
t.Logf("Destroying targets: %v", destroyTargets)
// If we're not forcing the targets to be destroyed, then expect to get a failure here as
// we'll have downstream resources to delete that weren't specified explicitly.
p.Steps = []TestStep{{
Op: Destroy,
ExpectFailure: !targetDependents,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
deleted := make(map[resource.URN]bool)
for _, entry := range entries {
assert.Equal(t, deploy.OpDelete, entry.Step.Op())
deleted[entry.Step.URN()] = true
}
for _, target := range p.Options.DestroyTargets {
assert.Contains(t, deleted, target)
}
validate(urns, deleted)
return res
},
}}
p.Run(t, old)
}
func TestUpdateTarget(t *testing.T) {
// Try refreshing a stack with combinations of the above resources as target to destroy.
subsets := combinations.All(complexTestDependencyGraphNames)
for _, subset := range subsets {
// limit to up to 3 resources to destroy. This keeps the test running time under
// control as it only generates a few hundred combinations instead of several thousand.
if len(subset) <= 3 {
updateSpecificTargets(t, subset)
}
}
updateSpecificTargets(t, []string{"A"})
// Also update a target that doesn't exist to make sure we don't crash or otherwise go off the rails.
updateInvalidTarget(t)
}
func updateSpecificTargets(t *testing.T, targets []string) {
// A
// _________|_________
// B C D
// ___|___ ___|___
// E F G H I J
// |__|
// K L
p := &TestPlan{}
urns, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// all resources will change.
return plugin.DiffResult{
Changes: plugin.DiffSome,
}, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
outputs := olds.Copy()
outputs["output_prop"] = resource.NewPropertyValue(42)
return outputs, resource.StatusOK, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
updateTargets := []resource.URN{}
for _, target := range targets {
updateTargets = append(updateTargets,
pickURN(t, urns, complexTestDependencyGraphNames, target))
}
p.Options.UpdateTargets = updateTargets
t.Logf("Updating targets: %v", updateTargets)
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
updated := make(map[resource.URN]bool)
sames := make(map[resource.URN]bool)
for _, entry := range entries {
if entry.Step.Op() == deploy.OpUpdate {
updated[entry.Step.URN()] = true
} else if entry.Step.Op() == deploy.OpSame {
sames[entry.Step.URN()] = true
} else {
assert.FailNowf(t, "", "Got a step that wasn't a same/update: %v", entry.Step.Op())
}
}
for _, target := range p.Options.UpdateTargets {
assert.Contains(t, updated, target)
}
for _, target := range p.Options.UpdateTargets {
assert.NotContains(t, sames, target)
}
return res
},
}}
p.Run(t, old)
}
func updateInvalidTarget(t *testing.T) {
p := &TestPlan{}
_, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// all resources will change.
return plugin.DiffResult{
Changes: plugin.DiffSome,
}, nil
},
UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64,
ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) {
outputs := olds.Copy()
outputs["output_prop"] = resource.NewPropertyValue(42)
return outputs, resource.StatusOK, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
p.Options.UpdateTargets = []resource.URN{"foo"}
t.Logf("Updating invalid targets: %v", p.Options.UpdateTargets)
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: true,
}}
p.Run(t, old)
}
func TestCreateDuringTargetedUpdate_CreateMentionedAsTarget(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
snap1 := p.Run(t, nil)
// Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
resA := p.NewURN("pkgA:m:typA", "resA", "")
resB := p.NewURN("pkgA:m:typA", "resB", "")
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA, resB}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
for _, entry := range entries {
if entry.Step.URN() == resA {
assert.Equal(t, deploy.OpSame, entry.Step.Op())
} else if entry.Step.URN() == resB {
assert.Equal(t, deploy.OpCreate, entry.Step.Op())
}
}
return res
},
}}
p.Run(t, snap1)
}
func TestCreateDuringTargetedUpdate_UntargetedCreateNotReferenced(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
snap1 := p.Run(t, nil)
// Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
resA := p.NewURN("pkgA:m:typA", "resA", "")
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
for _, entry := range entries {
// everything should be a same op here.
assert.Equal(t, deploy.OpSame, entry.Step.Op())
}
return res
},
}}
p.Run(t, snap1)
}
func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByTarget(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
p.Run(t, nil)
resA := p.NewURN("pkgA:m:typA", "resA", "")
resB := p.NewURN("pkgA:m:typA", "resB", "")
// Now, create a resource resB. But reference it from A. This will cause a dependency we can't
// satisfy.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true,
deploytest.ResourceOptions{
Dependencies: []resource.URN{resB},
})
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: true,
}}
p.Run(t, nil)
}
func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByUntargetedCreate(t *testing.T) {
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{}, nil
}),
}
program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...)
p := &TestPlan{
Options: UpdateOptions{Host: host1},
}
p.Steps = []TestStep{{Op: Update}}
snap1 := p.Run(t, nil)
resA := p.NewURN("pkgA:m:typA", "resA", "")
resB := p.NewURN("pkgA:m:typA", "resB", "")
// Now, create a resource resB. But reference it from A. This will cause a dependency we can't
// satisfy.
program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error {
_, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true)
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resC", true,
deploytest.ResourceOptions{
Dependencies: []resource.URN{resB},
})
assert.NoError(t, err)
_, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true)
assert.NoError(t, err)
return nil
})
host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...)
p.Options.Host = host2
p.Options.UpdateTargets = []resource.URN{resA}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
for _, entry := range entries {
assert.Equal(t, deploy.OpSame, entry.Step.Op())
}
return res
},
}}
p.Run(t, snap1)
}
func TestReplaceSpecificTargets(t *testing.T) {
// A
// _________|_________
// B C D
// ___|___ ___|___
// E F G H I J
// |__|
// K L
p := &TestPlan{}
urns, old, program := generateComplexTestDependencyGraph(t, p)
loaders := []*deploytest.ProviderLoader{
deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) {
return &deploytest.Provider{
DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap,
ignoreChanges []string) (plugin.DiffResult, error) {
// No resources will change.
return plugin.DiffResult{Changes: plugin.DiffNone}, nil
},
CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64,
preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) {
return "created-id", news, resource.StatusOK, nil
},
}, nil
}),
}
p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...)
getURN := func(name string) resource.URN {
return pickURN(t, urns, complexTestDependencyGraphNames, name)
}
p.Options.ReplaceTargets = []resource.URN{
getURN("F"),
getURN("B"),
getURN("G"),
}
p.Steps = []TestStep{{
Op: Update,
ExpectFailure: false,
Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries,
evts []Event, res result.Result) result.Result {
assert.Nil(t, res)
assert.True(t, len(entries) > 0)
replaced := make(map[resource.URN]bool)
sames := make(map[resource.URN]bool)
for _, entry := range entries {
if entry.Step.Op() == deploy.OpReplace {
replaced[entry.Step.URN()] = true
} else if entry.Step.Op() == deploy.OpSame {
sames[entry.Step.URN()] = true
}
}
for _, target := range p.Options.ReplaceTargets {
assert.Contains(t, replaced, target)
}
for _, target := range p.Options.ReplaceTargets {
assert.NotContains(t, sames, target)
}
return res
},
}}
p.Run(t, old)
}