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