github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/engine/lifecycletest/delete_before_replace_test.go (about) 1 //nolint:goconst 2 package lifecycletest 3 4 import ( 5 "testing" 6 7 "github.com/blang/semver" 8 "github.com/stretchr/testify/assert" 9 10 . "github.com/pulumi/pulumi/pkg/v3/engine" 11 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 12 "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" 13 "github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers" 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 type propertyDependencies map[resource.PropertyKey][]resource.URN 22 23 var complexTestDependencyGraphNames = []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"} 24 25 func generateComplexTestDependencyGraph( 26 t *testing.T, p *TestPlan) ([]resource.URN, *deploy.Snapshot, plugin.LanguageRuntime) { 27 28 resType := tokens.Type("pkgA:m:typA") 29 30 names := complexTestDependencyGraphNames 31 32 urnA := p.NewProviderURN("pkgA", names[0], "") 33 urnB := p.NewURN(resType, names[1], "") 34 urnC := p.NewProviderURN("pkgA", names[2], "") 35 urnD := p.NewProviderURN("pkgA", names[3], "") 36 urnE := p.NewURN(resType, names[4], "") 37 urnF := p.NewURN(resType, names[5], "") 38 urnG := p.NewURN(resType, names[6], "") 39 urnH := p.NewURN(resType, names[7], "") 40 urnI := p.NewURN(resType, names[8], "") 41 urnJ := p.NewURN(resType, names[9], "") 42 urnK := p.NewURN(resType, names[10], "") 43 urnL := p.NewURN(resType, names[11], "") 44 45 urns := []resource.URN{ 46 urnA, urnB, urnC, urnD, urnE, urnF, 47 urnG, urnH, urnI, urnJ, urnK, urnL, 48 } 49 50 newResource := func(urn resource.URN, id resource.ID, provider string, dependencies []resource.URN, 51 propertyDeps propertyDependencies, outputs resource.PropertyMap) *resource.State { 52 return newResource(urn, "", id, provider, dependencies, propertyDeps, outputs, true) 53 } 54 55 old := &deploy.Snapshot{ 56 Resources: []*resource.State{ 57 newResource(urnA, "0", "", nil, nil, resource.PropertyMap{"A": resource.NewStringProperty("foo")}), 58 newResource(urnB, "1", string(urnA)+"::0", nil, nil, nil), 59 newResource(urnC, "2", "", 60 []resource.URN{urnA}, 61 propertyDependencies{"A": []resource.URN{urnA}}, 62 resource.PropertyMap{"A": resource.NewStringProperty("bar")}), 63 newResource(urnD, "3", "", 64 []resource.URN{urnA}, 65 propertyDependencies{"B": []resource.URN{urnA}}, nil), 66 newResource(urnE, "4", string(urnC)+"::2", nil, nil, nil), 67 newResource(urnF, "5", "", 68 []resource.URN{urnC}, 69 propertyDependencies{"A": []resource.URN{urnC}}, nil), 70 newResource(urnG, "6", "", 71 []resource.URN{urnC}, 72 propertyDependencies{"B": []resource.URN{urnC}}, nil), 73 newResource(urnH, "4", string(urnD)+"::3", nil, nil, nil), 74 newResource(urnI, "5", "", 75 []resource.URN{urnD}, 76 propertyDependencies{"A": []resource.URN{urnD}}, nil), 77 newResource(urnJ, "6", "", 78 []resource.URN{urnD}, 79 propertyDependencies{"B": []resource.URN{urnD}}, nil), 80 newResource(urnK, "7", "", 81 []resource.URN{urnF, urnG}, 82 propertyDependencies{"A": []resource.URN{urnF, urnG}}, nil), 83 newResource(urnL, "8", "", 84 []resource.URN{urnF, urnG}, 85 propertyDependencies{"B": []resource.URN{urnF, urnG}}, nil), 86 }, 87 } 88 89 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 90 register := func(urn resource.URN, provider string, inputs resource.PropertyMap) resource.ID { 91 _, id, _, err := monitor.RegisterResource(urn.Type(), string(urn.Name()), true, deploytest.ResourceOptions{ 92 Provider: provider, 93 Inputs: inputs, 94 }) 95 assert.NoError(t, err) 96 return id 97 } 98 99 idA := register(urnA, "", resource.PropertyMap{"A": resource.NewStringProperty("bar")}) 100 register(urnB, string(urnA)+"::"+string(idA), nil) 101 idC := register(urnC, "", nil) 102 idD := register(urnD, "", nil) 103 register(urnE, string(urnC)+"::"+string(idC), nil) 104 register(urnF, "", nil) 105 register(urnG, "", nil) 106 register(urnH, string(urnD)+"::"+string(idD), nil) 107 register(urnI, "", nil) 108 register(urnJ, "", nil) 109 register(urnK, "", nil) 110 register(urnL, "", nil) 111 112 return nil 113 }) 114 115 return urns, old, program 116 } 117 118 func TestDeleteBeforeReplace(t *testing.T) { 119 t.Parallel() 120 121 // A 122 // _________|_________ 123 // B C D 124 // ___|___ ___|___ 125 // E F G H I J 126 // |__| 127 // K L 128 // 129 // For a given resource R in (A, C, D): 130 // - R will be the provider for its first dependent 131 // - A change to R will require that its second dependent be replaced 132 // - A change to R will not require that its third dependent be replaced 133 // 134 // In addition, K will have a requires-replacement property that depends on both F and G, and 135 // L will have a normal property that depends on both F and G. 136 // 137 // With that in mind, the following resources should require replacement: A, B, C, E, F, and K 138 139 p := &TestPlan{} 140 141 urns, old, program := generateComplexTestDependencyGraph(t, p) 142 names := complexTestDependencyGraphNames 143 144 loaders := []*deploytest.ProviderLoader{ 145 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 146 return &deploytest.Provider{ 147 DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap, 148 ignoreChanges []string) (plugin.DiffResult, error) { 149 if !olds["A"].DeepEquals(news["A"]) { 150 return plugin.DiffResult{ 151 ReplaceKeys: []resource.PropertyKey{"A"}, 152 DeleteBeforeReplace: true, 153 }, nil 154 } 155 return plugin.DiffResult{}, nil 156 }, 157 DiffF: func(urn resource.URN, id resource.ID, 158 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 159 160 if !olds["A"].DeepEquals(news["A"]) { 161 return plugin.DiffResult{ReplaceKeys: []resource.PropertyKey{"A"}}, nil 162 } 163 return plugin.DiffResult{}, nil 164 }, 165 }, nil 166 }), 167 } 168 169 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 170 171 p.Steps = []TestStep{{ 172 Op: Update, 173 ExpectFailure: false, 174 SkipPreview: true, 175 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 176 evts []Event, res result.Result) result.Result { 177 178 assert.Nil(t, res) 179 180 replaced := make(map[resource.URN]bool) 181 for _, entry := range entries { 182 if entry.Step.Op() == deploy.OpReplace { 183 replaced[entry.Step.URN()] = true 184 } 185 } 186 187 assert.Equal(t, map[resource.URN]bool{ 188 pickURN(t, urns, names, "A"): true, 189 pickURN(t, urns, names, "B"): true, 190 pickURN(t, urns, names, "C"): true, 191 pickURN(t, urns, names, "E"): true, 192 pickURN(t, urns, names, "F"): true, 193 pickURN(t, urns, names, "K"): true, 194 }, replaced) 195 196 return res 197 }, 198 }} 199 200 p.Run(t, old) 201 } 202 203 func TestPropertyDependenciesAdapter(t *testing.T) { 204 t.Parallel() 205 // Ensure that the eval source properly shims in property dependencies if none were reported (and does not if 206 // any were reported). 207 208 type propertyDependencies map[resource.PropertyKey][]resource.URN 209 210 loaders := []*deploytest.ProviderLoader{ 211 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 212 return &deploytest.Provider{}, nil 213 }), 214 } 215 216 const resType = "pkgA:m:typA" 217 var urnA, urnB, urnC, urnD resource.URN 218 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 219 220 register := func(name string, inputs resource.PropertyMap, inputDeps propertyDependencies, 221 dependencies []resource.URN) resource.URN { 222 223 urn, _, _, err := monitor.RegisterResource(resType, name, true, deploytest.ResourceOptions{ 224 Inputs: inputs, 225 Dependencies: dependencies, 226 PropertyDeps: inputDeps, 227 }) 228 assert.NoError(t, err) 229 230 return urn 231 } 232 233 urnA = register("A", nil, nil, nil) 234 urnB = register("B", nil, nil, nil) 235 urnC = register("C", resource.PropertyMap{ 236 "A": resource.NewStringProperty("foo"), 237 "B": resource.NewStringProperty("bar"), 238 }, nil, []resource.URN{urnA, urnB}) 239 urnD = register("D", resource.PropertyMap{ 240 "A": resource.NewStringProperty("foo"), 241 "B": resource.NewStringProperty("bar"), 242 }, propertyDependencies{ 243 "A": []resource.URN{urnB}, 244 "B": []resource.URN{urnA, urnC}, 245 }, []resource.URN{urnA, urnB, urnC}) 246 247 return nil 248 }) 249 250 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 251 p := &TestPlan{ 252 Options: UpdateOptions{Host: host}, 253 Steps: []TestStep{{Op: Update}}, 254 } 255 snap := p.Run(t, nil) 256 for _, res := range snap.Resources { 257 switch res.URN { 258 case urnA, urnB: 259 assert.Empty(t, res.Dependencies) 260 assert.Empty(t, res.PropertyDependencies) 261 case urnC: 262 assert.Equal(t, []resource.URN{urnA, urnB}, res.Dependencies) 263 assert.EqualValues(t, propertyDependencies{ 264 "A": res.Dependencies, 265 "B": res.Dependencies, 266 }, res.PropertyDependencies) 267 case urnD: 268 assert.Equal(t, []resource.URN{urnA, urnB, urnC}, res.Dependencies) 269 assert.EqualValues(t, propertyDependencies{ 270 "A": []resource.URN{urnB}, 271 "B": []resource.URN{urnA, urnC}, 272 }, res.PropertyDependencies) 273 } 274 } 275 } 276 277 func TestExplicitDeleteBeforeReplace(t *testing.T) { 278 t.Parallel() 279 280 p := &TestPlan{} 281 282 dbrDiff := false 283 loaders := []*deploytest.ProviderLoader{ 284 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 285 return &deploytest.Provider{ 286 DiffF: func(urn resource.URN, id resource.ID, 287 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 288 289 if !olds["A"].DeepEquals(news["A"]) { 290 return plugin.DiffResult{ 291 ReplaceKeys: []resource.PropertyKey{"A"}, 292 DeleteBeforeReplace: dbrDiff, 293 }, nil 294 } 295 return plugin.DiffResult{}, nil 296 }, 297 }, nil 298 }), 299 } 300 301 const resType = "pkgA:index:typ" 302 303 inputsA := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"}) 304 dbrValue, dbrA := true, (*bool)(nil) 305 inputsB := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"}) 306 307 var provURN, urnA, urnB resource.URN 308 var provID resource.ID 309 var err error 310 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 311 provURN, provID, _, err = monitor.RegisterResource(providers.MakeProviderType("pkgA"), "provA", true) 312 assert.NoError(t, err) 313 314 if provID == "" { 315 provID = providers.UnknownID 316 } 317 provRef, err := providers.NewReference(provURN, provID) 318 assert.NoError(t, err) 319 provA := provRef.String() 320 321 urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{ 322 Provider: provA, 323 Inputs: inputsA, 324 DeleteBeforeReplace: dbrA, 325 }) 326 assert.NoError(t, err) 327 328 inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}} 329 urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{ 330 Provider: provA, 331 Inputs: inputsB, 332 Dependencies: []resource.URN{urnA}, 333 PropertyDeps: inputDepsB, 334 }) 335 assert.NoError(t, err) 336 337 return nil 338 }) 339 340 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 341 p.Steps = []TestStep{{Op: Update}} 342 snap := p.Run(t, nil) 343 344 // Change the value of resA.A. Only resA should be replaced, and the replacement should be create-before-delete. 345 inputsA["A"] = resource.NewStringProperty("bar") 346 p.Steps = []TestStep{{ 347 Op: Update, 348 349 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 350 evts []Event, res result.Result) result.Result { 351 352 assert.Nil(t, res) 353 354 AssertSameSteps(t, []StepSummary{ 355 {Op: deploy.OpSame, URN: provURN}, 356 {Op: deploy.OpCreateReplacement, URN: urnA}, 357 {Op: deploy.OpReplace, URN: urnA}, 358 {Op: deploy.OpSame, URN: urnB}, 359 {Op: deploy.OpDeleteReplaced, URN: urnA}, 360 }, SuccessfulSteps(entries)) 361 362 return res 363 }, 364 }} 365 snap = p.Run(t, snap) 366 367 // Change the registration of resA such that it requires delete-before-replace and change the value of resA.A. Both 368 // resA and resB should be replaced, and the replacements should be delete-before-replace. 369 dbrA, inputsA["A"] = &dbrValue, resource.NewStringProperty("baz") 370 p.Steps = []TestStep{{ 371 Op: Update, 372 373 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 374 evts []Event, res result.Result) result.Result { 375 376 assert.Nil(t, res) 377 378 AssertSameSteps(t, []StepSummary{ 379 {Op: deploy.OpSame, URN: provURN}, 380 {Op: deploy.OpDeleteReplaced, URN: urnB}, 381 {Op: deploy.OpDeleteReplaced, URN: urnA}, 382 {Op: deploy.OpReplace, URN: urnA}, 383 {Op: deploy.OpCreateReplacement, URN: urnA}, 384 {Op: deploy.OpReplace, URN: urnB}, 385 {Op: deploy.OpCreateReplacement, URN: urnB}, 386 }, SuccessfulSteps(entries)) 387 388 return res 389 }, 390 }} 391 snap = p.Run(t, snap) 392 393 // Change the value of resB.A. Only resB should be replaced, and the replacement should be create-before-delete. 394 inputsB["A"] = resource.NewStringProperty("qux") 395 p.Steps = []TestStep{{ 396 Op: Update, 397 398 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 399 evts []Event, res result.Result) result.Result { 400 401 assert.Nil(t, res) 402 403 AssertSameSteps(t, []StepSummary{ 404 {Op: deploy.OpSame, URN: provURN}, 405 {Op: deploy.OpSame, URN: urnA}, 406 {Op: deploy.OpCreateReplacement, URN: urnB}, 407 {Op: deploy.OpReplace, URN: urnB}, 408 {Op: deploy.OpDeleteReplaced, URN: urnB}, 409 }, SuccessfulSteps(entries)) 410 411 return res 412 }, 413 }} 414 snap = p.Run(t, snap) 415 416 // Change the registration of resA such that it no longer requires delete-before-replace and change the value of 417 // resA.A. Only resA should be replaced, and the replacement should be create-before-delete. 418 dbrA, inputsA["A"] = nil, resource.NewStringProperty("zam") 419 p.Steps = []TestStep{{ 420 Op: Update, 421 422 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 423 evts []Event, res result.Result) result.Result { 424 425 assert.Nil(t, res) 426 427 AssertSameSteps(t, []StepSummary{ 428 {Op: deploy.OpSame, URN: provURN}, 429 {Op: deploy.OpCreateReplacement, URN: urnA}, 430 {Op: deploy.OpReplace, URN: urnA}, 431 {Op: deploy.OpSame, URN: urnB}, 432 {Op: deploy.OpDeleteReplaced, URN: urnA}, 433 }, SuccessfulSteps(entries)) 434 435 return res 436 }, 437 }} 438 snap = p.Run(t, snap) 439 440 // Change the diff of resA such that it requires delete-before-replace and change the value of resA.A. Both 441 // resA and resB should be replaced, and the replacements should be delete-before-replace. 442 dbrDiff, inputsA["A"] = true, resource.NewStringProperty("foo") 443 p.Steps = []TestStep{{ 444 Op: Update, 445 446 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 447 evts []Event, res result.Result) result.Result { 448 449 assert.Nil(t, res) 450 451 AssertSameSteps(t, []StepSummary{ 452 {Op: deploy.OpSame, URN: provURN}, 453 {Op: deploy.OpDeleteReplaced, URN: urnB}, 454 {Op: deploy.OpDeleteReplaced, URN: urnA}, 455 {Op: deploy.OpReplace, URN: urnA}, 456 {Op: deploy.OpCreateReplacement, URN: urnA}, 457 {Op: deploy.OpReplace, URN: urnB}, 458 {Op: deploy.OpCreateReplacement, URN: urnB}, 459 }, SuccessfulSteps(entries)) 460 461 return res 462 }, 463 }} 464 snap = p.Run(t, snap) 465 466 // Change the registration of resA such that it disables delete-before-replace and change the value of 467 // resA.A. Only resA should be replaced, and the replacement should be create-before-delete. 468 dbrA, dbrValue, inputsA["A"] = &dbrValue, false, resource.NewStringProperty("bar") 469 p.Steps = []TestStep{{ 470 Op: Update, 471 472 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 473 evts []Event, res result.Result) result.Result { 474 475 assert.Nil(t, res) 476 477 AssertSameSteps(t, []StepSummary{ 478 {Op: deploy.OpSame, URN: provURN}, 479 {Op: deploy.OpCreateReplacement, URN: urnA}, 480 {Op: deploy.OpReplace, URN: urnA}, 481 {Op: deploy.OpSame, URN: urnB}, 482 {Op: deploy.OpDeleteReplaced, URN: urnA}, 483 }, SuccessfulSteps(entries)) 484 485 return res 486 }, 487 }} 488 p.Run(t, snap) 489 } 490 491 func TestDependencyChangeDBR(t *testing.T) { 492 t.Parallel() 493 494 p := &TestPlan{} 495 496 loaders := []*deploytest.ProviderLoader{ 497 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 498 return &deploytest.Provider{ 499 DiffF: func(urn resource.URN, id resource.ID, 500 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 501 502 if !olds["A"].DeepEquals(news["A"]) { 503 return plugin.DiffResult{ 504 ReplaceKeys: []resource.PropertyKey{"A"}, 505 DeleteBeforeReplace: true, 506 }, nil 507 } 508 if !olds["B"].DeepEquals(news["B"]) { 509 return plugin.DiffResult{ 510 Changes: plugin.DiffSome, 511 }, nil 512 } 513 return plugin.DiffResult{}, nil 514 }, 515 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 516 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 517 518 return "created-id", news, resource.StatusOK, nil 519 }, 520 }, nil 521 }), 522 } 523 524 const resType = "pkgA:index:typ" 525 526 inputsA := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"}) 527 inputsB := resource.NewPropertyMapFromMap(map[string]interface{}{"A": "foo"}) 528 529 var urnA, urnB resource.URN 530 var err error 531 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 532 urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{ 533 Inputs: inputsA, 534 }) 535 assert.NoError(t, err) 536 537 inputDepsB := map[resource.PropertyKey][]resource.URN{"A": {urnA}} 538 urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{ 539 Inputs: inputsB, 540 Dependencies: []resource.URN{urnA}, 541 PropertyDeps: inputDepsB, 542 }) 543 assert.NoError(t, err) 544 545 return nil 546 }) 547 548 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 549 p.Steps = []TestStep{{Op: Update}} 550 snap := p.Run(t, nil) 551 552 inputsA["A"] = resource.NewStringProperty("bar") 553 program = deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 554 urnB, _, _, err = monitor.RegisterResource(resType, "resB", true, deploytest.ResourceOptions{ 555 Inputs: inputsB, 556 }) 557 assert.NoError(t, err) 558 559 urnA, _, _, err = monitor.RegisterResource(resType, "resA", true, deploytest.ResourceOptions{ 560 Inputs: inputsA, 561 }) 562 assert.NoError(t, err) 563 564 return nil 565 }) 566 567 p.Options.Host = deploytest.NewPluginHost(nil, nil, program, loaders...) 568 p.Steps = []TestStep{ 569 { 570 Op: Update, 571 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 572 evts []Event, res result.Result) result.Result { 573 574 assert.Nil(t, res) 575 assert.True(t, len(entries) > 0) 576 577 resBDeleted, resBSame := false, false 578 for _, entry := range entries { 579 if entry.Step.URN() == urnB { 580 switch entry.Step.Op() { 581 case deploy.OpDelete, deploy.OpDeleteReplaced: 582 resBDeleted = true 583 case deploy.OpSame: 584 resBSame = true 585 } 586 } 587 } 588 assert.True(t, resBSame) 589 assert.False(t, resBDeleted) 590 591 return res 592 }, 593 }, 594 } 595 p.Run(t, snap) 596 }