github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/engine/lifecycletest/target_test.go (about) 1 package lifecycletest 2 3 import ( 4 "fmt" 5 "testing" 6 7 "github.com/blang/semver" 8 combinations "github.com/mxschmitt/golang-combinations" 9 "github.com/stretchr/testify/assert" 10 11 . "github.com/pulumi/pulumi/pkg/v3/engine" 12 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 13 "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" 14 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 15 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" 16 "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" 17 "github.com/pulumi/pulumi/sdk/v3/go/common/util/result" 18 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 19 ) 20 21 func TestDestroyTarget(t *testing.T) { 22 t.Parallel() 23 24 // Try refreshing a stack with combinations of the above resources as target to destroy. 25 subsets := combinations.All(complexTestDependencyGraphNames) 26 27 //nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg 28 for _, subset := range subsets { 29 subset := subset 30 // limit to up to 3 resources to destroy. This keeps the test running time under 31 // control as it only generates a few hundred combinations instead of several thousand. 32 if len(subset) <= 3 { 33 t.Run(fmt.Sprintf("%v", subset), func(t *testing.T) { 34 t.Parallel() 35 36 destroySpecificTargets(t, subset, true, /*targetDependents*/ 37 func(urns []resource.URN, deleted map[resource.URN]bool) {}) 38 }) 39 } 40 } 41 42 t.Run("destroy root", func(t *testing.T) { 43 t.Parallel() 44 45 destroySpecificTargets( 46 t, []string{"A"}, true, /*targetDependents*/ 47 func(urns []resource.URN, deleted map[resource.URN]bool) { 48 // when deleting 'A' we expect A, B, C, D, E, F, G, H, I, J, K, and L to be deleted 49 names := complexTestDependencyGraphNames 50 assert.Equal(t, map[resource.URN]bool{ 51 pickURN(t, urns, names, "A"): true, 52 pickURN(t, urns, names, "B"): true, 53 pickURN(t, urns, names, "C"): true, 54 pickURN(t, urns, names, "D"): true, 55 pickURN(t, urns, names, "E"): true, 56 pickURN(t, urns, names, "F"): true, 57 pickURN(t, urns, names, "G"): true, 58 pickURN(t, urns, names, "H"): true, 59 pickURN(t, urns, names, "I"): true, 60 pickURN(t, urns, names, "J"): true, 61 pickURN(t, urns, names, "K"): true, 62 pickURN(t, urns, names, "L"): true, 63 }, deleted) 64 }) 65 }) 66 67 destroySpecificTargets( 68 t, []string{"A"}, false, /*targetDependents*/ 69 func(urns []resource.URN, deleted map[resource.URN]bool) {}) 70 } 71 72 func destroySpecificTargets( 73 t *testing.T, targets []string, targetDependents bool, 74 validate func(urns []resource.URN, deleted map[resource.URN]bool)) { 75 76 // A 77 // _________|_________ 78 // B C D 79 // ___|___ ___|___ 80 // E F G H I J 81 // |__| 82 // K L 83 84 p := &TestPlan{} 85 86 urns, old, program := generateComplexTestDependencyGraph(t, p) 87 88 loaders := []*deploytest.ProviderLoader{ 89 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 90 return &deploytest.Provider{ 91 DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap, 92 ignoreChanges []string) (plugin.DiffResult, error) { 93 if !olds["A"].DeepEquals(news["A"]) { 94 return plugin.DiffResult{ 95 ReplaceKeys: []resource.PropertyKey{"A"}, 96 DeleteBeforeReplace: true, 97 }, nil 98 } 99 return plugin.DiffResult{}, nil 100 }, 101 DiffF: func(urn resource.URN, id resource.ID, 102 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 103 104 if !olds["A"].DeepEquals(news["A"]) { 105 return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil 106 } 107 return plugin.DiffResult{}, nil 108 }, 109 }, nil 110 }), 111 } 112 113 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 114 p.Options.TargetDependents = targetDependents 115 116 destroyTargets := []resource.URN{} 117 for _, target := range targets { 118 destroyTargets = append(destroyTargets, pickURN(t, urns, complexTestDependencyGraphNames, target)) 119 } 120 121 p.Options.DestroyTargets = deploy.NewUrnTargetsFromUrns(destroyTargets) 122 t.Logf("Destroying targets: %v", destroyTargets) 123 124 // If we're not forcing the targets to be destroyed, then expect to get a failure here as 125 // we'll have downstream resources to delete that weren't specified explicitly. 126 p.Steps = []TestStep{{ 127 Op: Destroy, 128 ExpectFailure: !targetDependents, 129 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 130 evts []Event, res result.Result) result.Result { 131 132 assert.Nil(t, res) 133 assert.True(t, len(entries) > 0) 134 135 deleted := make(map[resource.URN]bool) 136 for _, entry := range entries { 137 assert.Equal(t, deploy.OpDelete, entry.Step.Op()) 138 deleted[entry.Step.URN()] = true 139 } 140 141 for _, target := range p.Options.DestroyTargets.Literals() { 142 assert.Contains(t, deleted, target) 143 } 144 145 validate(urns, deleted) 146 return res 147 }, 148 }} 149 150 p.Run(t, old) 151 } 152 153 func TestUpdateTarget(t *testing.T) { 154 t.Parallel() 155 156 // Try refreshing a stack with combinations of the above resources as target to destroy. 157 subsets := combinations.All(complexTestDependencyGraphNames) 158 159 //nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg 160 for _, subset := range subsets { 161 subset := subset 162 // limit to up to 3 resources to destroy. This keeps the test running time under 163 // control as it only generates a few hundred combinations instead of several thousand. 164 if len(subset) <= 3 { 165 t.Run(fmt.Sprintf("update %v", subset), func(t *testing.T) { 166 t.Parallel() 167 168 updateSpecificTargets(t, subset, nil, false /*targetDependents*/, -1) 169 }) 170 } 171 } 172 173 updateSpecificTargets(t, []string{"A"}, nil, false /*targetDependents*/, -1) 174 175 // Also update a target that doesn't exist to make sure we don't crash or otherwise go off the rails. 176 updateInvalidTarget(t) 177 178 // We want to check that targetDependents is respected 179 updateSpecificTargets(t, []string{"C"}, nil, true /*targetDependents*/, -1) 180 181 updateSpecificTargets(t, nil, []string{"**C**"}, false, 1) 182 updateSpecificTargets(t, nil, []string{"**providers:pkgA**"}, false, 3) 183 } 184 185 func updateSpecificTargets(t *testing.T, targets, globTargets []string, targetDependents bool, expectedUpdates int) { 186 // A 187 // _________|_________ 188 // B C D 189 // ___|___ ___|___ 190 // E F G H I J 191 // |__| 192 // K L 193 194 p := &TestPlan{} 195 196 urns, old, program := generateComplexTestDependencyGraph(t, p) 197 198 loaders := []*deploytest.ProviderLoader{ 199 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 200 return &deploytest.Provider{ 201 DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, 202 ignoreChanges []string) (plugin.DiffResult, error) { 203 204 // all resources will change. 205 return plugin.DiffResult{ 206 Changes: plugin.DiffSome, 207 }, nil 208 }, 209 210 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 211 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 212 213 outputs := olds.Copy() 214 215 outputs["output_prop"] = resource.NewPropertyValue(42) 216 return outputs, resource.StatusOK, nil 217 }, 218 }, nil 219 }), 220 } 221 222 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 223 p.Options.TargetDependents = targetDependents 224 225 updateTargets := globTargets 226 for _, target := range targets { 227 updateTargets = append(updateTargets, 228 string(pickURN(t, urns, complexTestDependencyGraphNames, target))) 229 } 230 231 p.Options.UpdateTargets = deploy.NewUrnTargets(updateTargets) 232 t.Logf("Updating targets: %v", updateTargets) 233 234 p.Steps = []TestStep{{ 235 Op: Update, 236 ExpectFailure: false, 237 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 238 evts []Event, res result.Result) result.Result { 239 240 assert.Nil(t, res) 241 assert.True(t, len(entries) > 0) 242 243 updated := make(map[resource.URN]bool) 244 sames := make(map[resource.URN]bool) 245 for _, entry := range entries { 246 if entry.Step.Op() == deploy.OpUpdate { 247 updated[entry.Step.URN()] = true 248 } else if entry.Step.Op() == deploy.OpSame { 249 sames[entry.Step.URN()] = true 250 } else { 251 assert.FailNowf(t, "", "Got a step that wasn't a same/update: %v", entry.Step.Op()) 252 } 253 } 254 255 for _, target := range p.Options.UpdateTargets.Literals() { 256 assert.Contains(t, updated, target) 257 } 258 259 if !targetDependents { 260 // We should only perform updates on the entries we have targeted. 261 for _, target := range p.Options.UpdateTargets.Literals() { 262 assert.Contains(t, targets, target.Name().String()) 263 } 264 } else { 265 // We expect to find at least one other resource updates. 266 267 // NOTE: The test is limited to only passing a subset valid behavior. By specifying 268 // a URN with no dependents, no other urns will be updated and the test will fail 269 // (incorrectly). 270 found := false 271 updateList := []string{} 272 for target := range updated { 273 updateList = append(updateList, target.Name().String()) 274 if !contains(targets, target.Name().String()) { 275 found = true 276 } 277 } 278 assert.True(t, found, "Updates: %v", updateList) 279 } 280 281 for _, target := range p.Options.UpdateTargets.Literals() { 282 assert.NotContains(t, sames, target) 283 } 284 if expectedUpdates > -1 { 285 assert.Equal(t, expectedUpdates, len(updated), "Updates = %#v", updated) 286 } 287 return res 288 }, 289 }} 290 p.Run(t, old) 291 } 292 293 func contains(list []string, entry string) bool { 294 for _, e := range list { 295 if e == entry { 296 return true 297 } 298 } 299 return false 300 } 301 302 func updateInvalidTarget(t *testing.T) { 303 p := &TestPlan{} 304 305 _, old, program := generateComplexTestDependencyGraph(t, p) 306 307 loaders := []*deploytest.ProviderLoader{ 308 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 309 return &deploytest.Provider{ 310 DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, 311 ignoreChanges []string) (plugin.DiffResult, error) { 312 313 // all resources will change. 314 return plugin.DiffResult{ 315 Changes: plugin.DiffSome, 316 }, nil 317 }, 318 319 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 320 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 321 322 outputs := olds.Copy() 323 324 outputs["output_prop"] = resource.NewPropertyValue(42) 325 return outputs, resource.StatusOK, nil 326 }, 327 }, nil 328 }), 329 } 330 331 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 332 333 p.Options.UpdateTargets = deploy.NewUrnTargetsFromUrns([]resource.URN{"foo"}) 334 t.Logf("Updating invalid targets: %v", p.Options.UpdateTargets) 335 336 p.Steps = []TestStep{{ 337 Op: Update, 338 ExpectFailure: true, 339 }} 340 341 p.Run(t, old) 342 } 343 344 func TestCreateDuringTargetedUpdate_CreateMentionedAsTarget(t *testing.T) { 345 t.Parallel() 346 347 loaders := []*deploytest.ProviderLoader{ 348 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 349 return &deploytest.Provider{}, nil 350 }), 351 } 352 353 program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 354 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 355 assert.NoError(t, err) 356 return nil 357 }) 358 host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...) 359 360 p := &TestPlan{ 361 Options: UpdateOptions{Host: host1}, 362 } 363 364 p.Steps = []TestStep{{Op: Update}} 365 snap1 := p.Run(t, nil) 366 367 // Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything. 368 program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 369 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 370 assert.NoError(t, err) 371 372 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true) 373 assert.NoError(t, err) 374 375 return nil 376 }) 377 host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...) 378 379 resA := p.NewURN("pkgA:m:typA", "resA", "") 380 resB := p.NewURN("pkgA:m:typA", "resB", "") 381 p.Options.Host = host2 382 p.Options.UpdateTargets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA, resB}) 383 p.Steps = []TestStep{{ 384 Op: Update, 385 ExpectFailure: false, 386 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 387 evts []Event, res result.Result) result.Result { 388 389 assert.Nil(t, res) 390 assert.True(t, len(entries) > 0) 391 392 for _, entry := range entries { 393 if entry.Step.URN() == resA { 394 assert.Equal(t, deploy.OpSame, entry.Step.Op()) 395 } else if entry.Step.URN() == resB { 396 assert.Equal(t, deploy.OpCreate, entry.Step.Op()) 397 } 398 } 399 400 return res 401 }, 402 }} 403 p.Run(t, snap1) 404 } 405 406 func TestCreateDuringTargetedUpdate_UntargetedCreateNotReferenced(t *testing.T) { 407 t.Parallel() 408 409 loaders := []*deploytest.ProviderLoader{ 410 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 411 return &deploytest.Provider{}, nil 412 }), 413 } 414 415 program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 416 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 417 assert.NoError(t, err) 418 return nil 419 }) 420 host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...) 421 422 p := &TestPlan{ 423 Options: UpdateOptions{Host: host1}, 424 } 425 426 p.Steps = []TestStep{{Op: Update}} 427 snap1 := p.Run(t, nil) 428 429 // Now, create a resource resB. This shouldn't be a problem since resB isn't referenced by anything. 430 program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 431 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 432 assert.NoError(t, err) 433 434 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true) 435 assert.NoError(t, err) 436 437 return nil 438 }) 439 host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...) 440 441 resA := p.NewURN("pkgA:m:typA", "resA", "") 442 443 p.Options.Host = host2 444 p.Options.UpdateTargets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA}) 445 p.Steps = []TestStep{{ 446 Op: Update, 447 ExpectFailure: false, 448 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 449 evts []Event, res result.Result) result.Result { 450 451 assert.Nil(t, res) 452 assert.True(t, len(entries) > 0) 453 454 for _, entry := range entries { 455 // everything should be a same op here. 456 assert.Equal(t, deploy.OpSame, entry.Step.Op()) 457 } 458 459 return res 460 }, 461 }} 462 p.Run(t, snap1) 463 } 464 465 func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByTarget(t *testing.T) { 466 t.Parallel() 467 468 loaders := []*deploytest.ProviderLoader{ 469 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 470 return &deploytest.Provider{}, nil 471 }), 472 } 473 474 program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 475 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 476 assert.NoError(t, err) 477 return nil 478 }) 479 host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...) 480 481 p := &TestPlan{ 482 Options: UpdateOptions{Host: host1}, 483 } 484 485 p.Steps = []TestStep{{Op: Update}} 486 p.Run(t, nil) 487 488 resA := p.NewURN("pkgA:m:typA", "resA", "") 489 resB := p.NewURN("pkgA:m:typA", "resB", "") 490 491 // Now, create a resource resB. But reference it from A. This will cause a dependency we can't 492 // satisfy. 493 program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 494 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true) 495 assert.NoError(t, err) 496 497 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true, 498 deploytest.ResourceOptions{ 499 Dependencies: []resource.URN{resB}, 500 }) 501 assert.NoError(t, err) 502 503 return nil 504 }) 505 host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...) 506 507 p.Options.Host = host2 508 p.Options.UpdateTargets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA}) 509 p.Steps = []TestStep{{ 510 Op: Update, 511 ExpectFailure: true, 512 }} 513 p.Run(t, nil) 514 } 515 516 func TestCreateDuringTargetedUpdate_UntargetedCreateReferencedByUntargetedCreate(t *testing.T) { 517 t.Parallel() 518 519 loaders := []*deploytest.ProviderLoader{ 520 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 521 return &deploytest.Provider{}, nil 522 }), 523 } 524 525 program1 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 526 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 527 assert.NoError(t, err) 528 return nil 529 }) 530 host1 := deploytest.NewPluginHost(nil, nil, program1, loaders...) 531 532 p := &TestPlan{ 533 Options: UpdateOptions{Host: host1}, 534 } 535 536 p.Steps = []TestStep{{Op: Update}} 537 snap1 := p.Run(t, nil) 538 539 resA := p.NewURN("pkgA:m:typA", "resA", "") 540 resB := p.NewURN("pkgA:m:typA", "resB", "") 541 542 // Now, create a resource resB. But reference it from A. This will cause a dependency we can't 543 // satisfy. 544 program2 := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 545 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true) 546 assert.NoError(t, err) 547 548 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resC", true, 549 deploytest.ResourceOptions{ 550 Dependencies: []resource.URN{resB}, 551 }) 552 assert.NoError(t, err) 553 554 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resA", true) 555 assert.NoError(t, err) 556 557 return nil 558 }) 559 host2 := deploytest.NewPluginHost(nil, nil, program2, loaders...) 560 561 p.Options.Host = host2 562 p.Options.UpdateTargets = deploy.NewUrnTargetsFromUrns([]resource.URN{resA}) 563 p.Steps = []TestStep{{ 564 Op: Update, 565 ExpectFailure: false, 566 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 567 evts []Event, res result.Result) result.Result { 568 569 assert.Nil(t, res) 570 assert.True(t, len(entries) > 0) 571 572 for _, entry := range entries { 573 assert.Equal(t, deploy.OpSame, entry.Step.Op()) 574 } 575 576 return res 577 }, 578 }} 579 p.Run(t, snap1) 580 } 581 582 func TestReplaceSpecificTargets(t *testing.T) { 583 t.Parallel() 584 585 // A 586 // _________|_________ 587 // B C D 588 // ___|___ ___|___ 589 // E F G H I J 590 // |__| 591 // K L 592 593 p := &TestPlan{} 594 595 urns, old, program := generateComplexTestDependencyGraph(t, p) 596 597 loaders := []*deploytest.ProviderLoader{ 598 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 599 return &deploytest.Provider{ 600 DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, 601 ignoreChanges []string) (plugin.DiffResult, error) { 602 603 // No resources will change. 604 return plugin.DiffResult{Changes: plugin.DiffNone}, nil 605 }, 606 607 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 608 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 609 610 return "created-id", news, resource.StatusOK, nil 611 }, 612 }, nil 613 }), 614 } 615 616 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 617 618 getURN := func(name string) resource.URN { 619 return pickURN(t, urns, complexTestDependencyGraphNames, name) 620 } 621 622 p.Options.ReplaceTargets = deploy.NewUrnTargetsFromUrns([]resource.URN{ 623 getURN("F"), 624 getURN("B"), 625 getURN("G"), 626 }) 627 628 p.Steps = []TestStep{{ 629 Op: Update, 630 ExpectFailure: false, 631 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 632 evts []Event, res result.Result) result.Result { 633 634 assert.Nil(t, res) 635 assert.True(t, len(entries) > 0) 636 637 replaced := make(map[resource.URN]bool) 638 sames := make(map[resource.URN]bool) 639 for _, entry := range entries { 640 if entry.Step.Op() == deploy.OpReplace { 641 replaced[entry.Step.URN()] = true 642 } else if entry.Step.Op() == deploy.OpSame { 643 sames[entry.Step.URN()] = true 644 } 645 } 646 647 for _, target := range p.Options.ReplaceTargets.Literals() { 648 assert.Contains(t, replaced, target) 649 } 650 651 for _, target := range p.Options.ReplaceTargets.Literals() { 652 assert.NotContains(t, sames, target) 653 } 654 655 return res 656 }, 657 }} 658 659 p.Run(t, old) 660 } 661 662 var componentBasedTestDependencyGraphNames = []string{"A", "B", "C", "D", "E", "F", "G", "H", 663 "I", "J", "K", "L", "M", "N"} 664 665 func generateParentedTestDependencyGraph(t *testing.T, p *TestPlan) ( 666 // Parent-child graph 667 // A B 668 // __|__ ____|____ 669 // D I E F 670 // __|__ __|__ __|__ 671 // G H J K L M 672 // 673 // A has children D, I 674 // D has children G, H 675 // B has children E, F 676 // E has children J, K 677 // F has children L, M 678 // 679 // Dependency graph 680 // G H 681 // | __|__ 682 // I K N 683 // 684 // I depends on G 685 // K depends on H 686 // N depends on H 687 688 []resource.URN, *deploy.Snapshot, plugin.LanguageRuntime) { 689 resTypeComponent := tokens.Type("pkgA:index:Component") 690 resTypeResource := tokens.Type("pkgA:index:Resource") 691 692 names := componentBasedTestDependencyGraphNames 693 694 urnA := p.NewURN(resTypeComponent, names[0], "") 695 urnB := p.NewURN(resTypeComponent, names[1], "") 696 urnC := p.NewURN(resTypeResource, names[2], "") 697 urnD := p.NewURN(resTypeComponent, names[3], urnA) 698 urnE := p.NewURN(resTypeComponent, names[4], urnB) 699 urnF := p.NewURN(resTypeComponent, names[5], urnB) 700 urnG := p.NewURN(resTypeResource, names[6], urnD) 701 urnH := p.NewURN(resTypeResource, names[7], urnD) 702 urnI := p.NewURN(resTypeResource, names[8], urnA) 703 urnJ := p.NewURN(resTypeResource, names[9], urnE) 704 urnK := p.NewURN(resTypeResource, names[10], urnE) 705 urnL := p.NewURN(resTypeResource, names[11], urnF) 706 urnM := p.NewURN(resTypeResource, names[12], urnF) 707 urnN := p.NewURN(resTypeResource, names[13], "") 708 709 urns := []resource.URN{urnA, urnB, urnC, urnD, urnE, urnF, urnG, urnH, urnI, urnJ, urnK, urnL, urnM, urnN} 710 711 newResource := func(urn, parent resource.URN, id resource.ID, 712 dependencies []resource.URN, propertyDeps propertyDependencies) *resource.State { 713 return newResource(urn, parent, id, "", dependencies, propertyDeps, 714 nil, urn.Type() != resTypeComponent) 715 } 716 717 old := &deploy.Snapshot{ 718 Resources: []*resource.State{ 719 newResource(urnA, "", "0", nil, nil), 720 newResource(urnB, "", "1", nil, nil), 721 newResource(urnC, "", "2", nil, nil), 722 newResource(urnD, urnA, "3", nil, nil), 723 newResource(urnE, urnB, "4", nil, nil), 724 newResource(urnF, urnB, "5", nil, nil), 725 newResource(urnG, urnD, "6", nil, nil), 726 newResource(urnH, urnD, "7", nil, nil), 727 newResource(urnI, urnA, "8", []resource.URN{urnG}, 728 propertyDependencies{"A": []resource.URN{urnG}}), 729 newResource(urnJ, urnE, "9", nil, nil), 730 newResource(urnK, urnE, "10", []resource.URN{urnH}, 731 propertyDependencies{"A": []resource.URN{urnH}}), 732 newResource(urnL, urnF, "11", nil, nil), 733 newResource(urnM, urnF, "12", nil, nil), 734 newResource(urnN, "", "13", []resource.URN{urnH}, 735 propertyDependencies{"A": []resource.URN{urnH}}), 736 }, 737 } 738 739 program := deploytest.NewLanguageRuntime( 740 func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 741 register := func(urn, parent resource.URN) resource.ID { 742 _, id, _, err := monitor.RegisterResource( 743 urn.Type(), 744 string(urn.Name()), 745 urn.Type() != resTypeComponent, 746 deploytest.ResourceOptions{ 747 Inputs: nil, 748 Parent: parent, 749 }) 750 assert.NoError(t, err) 751 return id 752 } 753 754 register(urnA, "") 755 register(urnB, "") 756 register(urnC, "") 757 register(urnD, urnA) 758 register(urnE, urnB) 759 register(urnF, urnB) 760 register(urnG, urnD) 761 register(urnH, urnD) 762 register(urnI, urnA) 763 register(urnJ, urnE) 764 register(urnK, urnE) 765 register(urnL, urnF) 766 register(urnM, urnF) 767 register(urnN, "") 768 769 return nil 770 }) 771 772 return urns, old, program 773 } 774 775 func TestDestroyTargetWithChildren(t *testing.T) { 776 t.Parallel() 777 778 // when deleting 'A' with targetDependents specified we expect A, D, G, H, I, K and N to be deleted. 779 destroySpecificTargetsWithChildren( 780 t, []string{"A"}, true, /*targetDependents*/ 781 func(urns []resource.URN, deleted map[resource.URN]bool) { 782 names := componentBasedTestDependencyGraphNames 783 assert.Equal(t, map[resource.URN]bool{ 784 pickURN(t, urns, names, "A"): true, 785 pickURN(t, urns, names, "D"): true, 786 pickURN(t, urns, names, "G"): true, 787 pickURN(t, urns, names, "H"): true, 788 pickURN(t, urns, names, "I"): true, 789 pickURN(t, urns, names, "K"): true, 790 pickURN(t, urns, names, "N"): true, 791 }, deleted) 792 }) 793 794 // when deleting 'A' with targetDependents not specified, we expect an error. 795 destroySpecificTargetsWithChildren( 796 t, []string{"A"}, false, /*targetDependents*/ 797 func(urns []resource.URN, deleted map[resource.URN]bool) {}) 798 799 // when deleting 'B' we expect B, E, F, J, K, L, M to be deleted. 800 destroySpecificTargetsWithChildren( 801 t, []string{"B"}, false, /*targetDependents*/ 802 func(urns []resource.URN, deleted map[resource.URN]bool) { 803 names := componentBasedTestDependencyGraphNames 804 assert.Equal(t, map[resource.URN]bool{ 805 pickURN(t, urns, names, "B"): true, 806 pickURN(t, urns, names, "E"): true, 807 pickURN(t, urns, names, "F"): true, 808 pickURN(t, urns, names, "J"): true, 809 pickURN(t, urns, names, "K"): true, 810 pickURN(t, urns, names, "L"): true, 811 pickURN(t, urns, names, "M"): true, 812 }, deleted) 813 }) 814 } 815 816 func destroySpecificTargetsWithChildren( 817 t *testing.T, targets []string, targetDependents bool, 818 validate func(urns []resource.URN, deleted map[resource.URN]bool)) { 819 820 p := &TestPlan{} 821 822 urns, old, program := generateParentedTestDependencyGraph(t, p) 823 824 loaders := []*deploytest.ProviderLoader{ 825 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 826 return &deploytest.Provider{ 827 DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap, 828 ignoreChanges []string) (plugin.DiffResult, error) { 829 if !olds["A"].DeepEquals(news["A"]) { 830 return plugin.DiffResult{ 831 ReplaceKeys: []resource.PropertyKey{"A"}, 832 DeleteBeforeReplace: true, 833 }, nil 834 } 835 return plugin.DiffResult{}, nil 836 }, 837 DiffF: func(urn resource.URN, id resource.ID, 838 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 839 840 if !olds["A"].DeepEquals(news["A"]) { 841 return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil 842 } 843 return plugin.DiffResult{}, nil 844 }, 845 }, nil 846 }), 847 } 848 849 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 850 p.Options.TargetDependents = targetDependents 851 852 destroyTargets := []resource.URN{} 853 for _, target := range targets { 854 destroyTargets = append(destroyTargets, pickURN(t, urns, componentBasedTestDependencyGraphNames, target)) 855 } 856 857 p.Options.DestroyTargets = deploy.NewUrnTargetsFromUrns(destroyTargets) 858 t.Logf("Destroying targets: %v", destroyTargets) 859 860 // If we're not forcing the targets to be destroyed, then expect to get a failure here as 861 // we'll have downstream resources to delete that weren't specified explicitly. 862 p.Steps = []TestStep{{ 863 Op: Destroy, 864 ExpectFailure: !targetDependents, 865 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 866 evts []Event, res result.Result) result.Result { 867 868 assert.Nil(t, res) 869 assert.True(t, len(entries) > 0) 870 871 deleted := make(map[resource.URN]bool) 872 for _, entry := range entries { 873 assert.Equal(t, deploy.OpDelete, entry.Step.Op()) 874 deleted[entry.Step.URN()] = true 875 } 876 877 for _, target := range p.Options.DestroyTargets.Literals() { 878 assert.Contains(t, deleted, target) 879 } 880 881 validate(urns, deleted) 882 return res 883 }, 884 }} 885 886 p.Run(t, old) 887 } 888 889 func newResource(urn, parent resource.URN, id resource.ID, provider string, dependencies []resource.URN, 890 propertyDeps propertyDependencies, outputs resource.PropertyMap, custom bool) *resource.State { 891 892 inputs := resource.PropertyMap{} 893 for k := range propertyDeps { 894 inputs[k] = resource.NewStringProperty("foo") 895 } 896 897 return &resource.State{ 898 Type: urn.Type(), 899 URN: urn, 900 Custom: custom, 901 Delete: false, 902 ID: id, 903 Inputs: inputs, 904 Outputs: outputs, 905 Dependencies: dependencies, 906 PropertyDependencies: propertyDeps, 907 Provider: provider, 908 Parent: parent, 909 } 910 }