github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/engine/lifecycletest/refresh_test.go (about) 1 package lifecycletest 2 3 import ( 4 "context" 5 "fmt" 6 "reflect" 7 "strconv" 8 "testing" 9 10 "github.com/blang/semver" 11 combinations "github.com/mxschmitt/golang-combinations" 12 "github.com/stretchr/testify/assert" 13 14 . "github.com/pulumi/pulumi/pkg/v3/engine" 15 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 16 "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" 17 "github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers" 18 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 19 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" 20 "github.com/pulumi/pulumi/sdk/v3/go/common/util/result" 21 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 22 ) 23 24 func TestParallelRefresh(t *testing.T) { 25 t.Parallel() 26 27 loaders := []*deploytest.ProviderLoader{ 28 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 29 return &deploytest.Provider{}, nil 30 }), 31 } 32 33 // Create a program that registers four resources, each of which depends on the resource that immediately precedes 34 // it. 35 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 36 resA, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 37 assert.NoError(t, err) 38 39 resB, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 40 Dependencies: []resource.URN{resA}, 41 }) 42 assert.NoError(t, err) 43 44 resC, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{ 45 Dependencies: []resource.URN{resB}, 46 }) 47 assert.NoError(t, err) 48 49 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resD", true, deploytest.ResourceOptions{ 50 Dependencies: []resource.URN{resC}, 51 }) 52 assert.NoError(t, err) 53 54 return nil 55 }) 56 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 57 58 p := &TestPlan{ 59 Options: UpdateOptions{Parallel: 4, Host: host}, 60 } 61 62 p.Steps = []TestStep{{Op: Update}} 63 snap := p.Run(t, nil) 64 65 assert.Len(t, snap.Resources, 5) 66 assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider 67 assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA") 68 assert.Equal(t, string(snap.Resources[2].URN.Name()), "resB") 69 assert.Equal(t, string(snap.Resources[3].URN.Name()), "resC") 70 assert.Equal(t, string(snap.Resources[4].URN.Name()), "resD") 71 72 p.Steps = []TestStep{{Op: Refresh}} 73 snap = p.Run(t, snap) 74 75 assert.Len(t, snap.Resources, 5) 76 assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider 77 assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA") 78 assert.Equal(t, string(snap.Resources[2].URN.Name()), "resB") 79 assert.Equal(t, string(snap.Resources[3].URN.Name()), "resC") 80 assert.Equal(t, string(snap.Resources[4].URN.Name()), "resD") 81 } 82 83 func TestExternalRefresh(t *testing.T) { 84 t.Parallel() 85 86 loaders := []*deploytest.ProviderLoader{ 87 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 88 return &deploytest.Provider{}, nil 89 }), 90 } 91 92 // Our program reads a resource and exits. 93 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 94 _, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "resA-some-id", "", resource.PropertyMap{}, "", "") 95 if !assert.NoError(t, err) { 96 t.FailNow() 97 } 98 99 return nil 100 }) 101 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 102 p := &TestPlan{ 103 Options: UpdateOptions{Host: host}, 104 Steps: []TestStep{{Op: Update}}, 105 } 106 107 // The read should place "resA" in the snapshot with the "External" bit set. 108 snap := p.Run(t, nil) 109 assert.Len(t, snap.Resources, 2) 110 assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider 111 assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA") 112 assert.True(t, snap.Resources[1].External) 113 114 p = &TestPlan{ 115 Options: UpdateOptions{Host: host}, 116 Steps: []TestStep{{Op: Refresh}}, 117 } 118 119 snap = p.Run(t, snap) 120 // A refresh should leave "resA" as it is in the snapshot. The External bit should still be set. 121 assert.Len(t, snap.Resources, 2) 122 assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") // provider 123 assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA") 124 assert.True(t, snap.Resources[1].External) 125 } 126 127 func TestRefreshInitFailure(t *testing.T) { 128 t.Parallel() 129 130 p := &TestPlan{} 131 132 provURN := p.NewProviderURN("pkgA", "default", "") 133 resURN := p.NewURN("pkgA:m:typA", "resA", "") 134 res2URN := p.NewURN("pkgA:m:typA", "resB", "") 135 136 res2Outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")} 137 138 // 139 // Refresh will persist any initialization errors that are returned by `Read`. This provider 140 // will error out or not based on the value of `refreshShouldFail`. 141 // 142 refreshShouldFail := false 143 144 // 145 // Set up test environment to use `readFailProvider` as the underlying resource provider. 146 // 147 loaders := []*deploytest.ProviderLoader{ 148 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 149 return &deploytest.Provider{ 150 ReadF: func( 151 urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, 152 ) (plugin.ReadResult, resource.Status, error) { 153 if refreshShouldFail && urn == resURN { 154 err := &plugin.InitError{ 155 Reasons: []string{"Refresh reports continued to fail to initialize"}, 156 } 157 return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusPartialFailure, err 158 } else if urn == res2URN { 159 return plugin.ReadResult{Outputs: res2Outputs}, resource.StatusOK, nil 160 } 161 return plugin.ReadResult{Outputs: resource.PropertyMap{}}, resource.StatusOK, nil 162 }, 163 }, nil 164 }), 165 } 166 167 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 168 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 169 assert.NoError(t, err) 170 return nil 171 }) 172 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 173 174 p.Options.Host = host 175 176 // 177 // Create an old snapshot with a single initialization failure. 178 // 179 old := &deploy.Snapshot{ 180 Resources: []*resource.State{ 181 { 182 Type: resURN.Type(), 183 URN: resURN, 184 Custom: true, 185 ID: "0", 186 Inputs: resource.PropertyMap{}, 187 Outputs: resource.PropertyMap{}, 188 InitErrors: []string{"Resource failed to initialize"}, 189 }, 190 { 191 Type: res2URN.Type(), 192 URN: res2URN, 193 Custom: true, 194 ID: "1", 195 Inputs: resource.PropertyMap{}, 196 Outputs: resource.PropertyMap{}, 197 }, 198 }, 199 } 200 201 // 202 // Refresh DOES NOT fail, causing the initialization error to disappear. 203 // 204 p.Steps = []TestStep{{Op: Refresh}} 205 snap := p.Run(t, old) 206 207 for _, resource := range snap.Resources { 208 switch urn := resource.URN; urn { 209 case provURN: 210 // break 211 case resURN: 212 assert.Empty(t, resource.InitErrors) 213 case res2URN: 214 assert.Equal(t, res2Outputs, resource.Outputs) 215 default: 216 t.Fatalf("unexpected resource %v", urn) 217 } 218 } 219 220 // 221 // Refresh again, see the resource is in a partial state of failure, but the refresh operation 222 // DOES NOT fail. The initialization error is still persisted. 223 // 224 refreshShouldFail = true 225 p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}} 226 snap = p.Run(t, old) 227 for _, resource := range snap.Resources { 228 switch urn := resource.URN; urn { 229 case provURN: 230 // break 231 case resURN: 232 assert.Equal(t, []string{"Refresh reports continued to fail to initialize"}, resource.InitErrors) 233 case res2URN: 234 assert.Equal(t, res2Outputs, resource.Outputs) 235 default: 236 t.Fatalf("unexpected resource %v", urn) 237 } 238 } 239 } 240 241 // Test that tests that Refresh can detect that resources have been deleted and removes them 242 // from the snapshot. 243 func TestRefreshWithDelete(t *testing.T) { 244 t.Parallel() 245 246 //nolint:paralleltest // false positive because range var isn't used directly in t.Run(name) arg 247 for _, parallelFactor := range []int{1, 4} { 248 parallelFactor := parallelFactor 249 t.Run(fmt.Sprintf("parallel-%d", parallelFactor), func(t *testing.T) { 250 t.Parallel() 251 252 loaders := []*deploytest.ProviderLoader{ 253 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 254 return &deploytest.Provider{ 255 ReadF: func( 256 urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, 257 ) (plugin.ReadResult, resource.Status, error) { 258 // This thing doesn't exist. Returning nil from Read should trigger 259 // the engine to delete it from the snapshot. 260 return plugin.ReadResult{}, resource.StatusOK, nil 261 }, 262 }, nil 263 }), 264 } 265 266 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 267 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 268 assert.NoError(t, err) 269 return err 270 }) 271 272 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 273 p := &TestPlan{Options: UpdateOptions{Host: host, Parallel: parallelFactor}} 274 275 p.Steps = []TestStep{{Op: Update}} 276 snap := p.Run(t, nil) 277 278 p.Steps = []TestStep{{Op: Refresh}} 279 snap = p.Run(t, snap) 280 281 // Refresh succeeds and records that the resource in the snapshot doesn't exist anymore 282 provURN := p.NewProviderURN("pkgA", "default", "") 283 assert.Len(t, snap.Resources, 1) 284 assert.Equal(t, provURN, snap.Resources[0].URN) 285 }) 286 } 287 } 288 289 // Tests that dependencies are correctly rewritten when refresh removes deleted resources. 290 func TestRefreshDeleteDependencies(t *testing.T) { 291 t.Parallel() 292 293 names := []string{"resA", "resB", "resC"} 294 295 // Try refreshing a stack with every combination of the three above resources as a target to 296 // refresh. 297 subsets := combinations.All(names) 298 299 // combinations.All doesn't return the empty set. So explicitly test that case (i.e. test no 300 // targets specified) 301 validateRefreshDeleteCombination(t, names, []string{}) 302 303 for _, subset := range subsets { 304 validateRefreshDeleteCombination(t, names, subset) 305 } 306 } 307 308 // Looks up the provider ID in newResources and sets "Provider" to reference that in every resource in oldResources. 309 func setProviderRef(t *testing.T, oldResources, newResources []*resource.State, provURN resource.URN) { 310 for _, r := range newResources { 311 if r.URN == provURN { 312 provRef, err := providers.NewReference(r.URN, r.ID) 313 assert.NoError(t, err) 314 for i := range oldResources { 315 oldResources[i].Provider = provRef.String() 316 } 317 break 318 } 319 } 320 } 321 322 func validateRefreshDeleteCombination(t *testing.T, names []string, targets []string) { 323 p := &TestPlan{} 324 325 const resType = "pkgA:m:typA" 326 327 urnA := p.NewURN(resType, names[0], "") 328 urnB := p.NewURN(resType, names[1], "") 329 urnC := p.NewURN(resType, names[2], "") 330 urns := []resource.URN{urnA, urnB, urnC} 331 332 refreshTargets := []resource.URN{} 333 334 t.Logf("Refreshing targets: %v", targets) 335 for _, target := range targets { 336 refreshTargets = append(refreshTargets, pickURN(t, urns, names, target)) 337 } 338 339 p.Options.RefreshTargets = deploy.NewUrnTargetsFromUrns(refreshTargets) 340 341 newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State { 342 return &resource.State{ 343 Type: urn.Type(), 344 URN: urn, 345 Custom: true, 346 Delete: delete, 347 ID: id, 348 Inputs: resource.PropertyMap{}, 349 Outputs: resource.PropertyMap{}, 350 Dependencies: dependencies, 351 } 352 } 353 354 oldResources := []*resource.State{ 355 newResource(urnA, "0", false), 356 newResource(urnB, "1", false, urnA), 357 newResource(urnC, "2", false, urnA, urnB), 358 newResource(urnA, "3", true), 359 newResource(urnA, "4", true), 360 newResource(urnC, "5", true, urnA, urnB), 361 } 362 363 old := &deploy.Snapshot{ 364 Resources: oldResources, 365 } 366 367 loaders := []*deploytest.ProviderLoader{ 368 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 369 return &deploytest.Provider{ 370 ReadF: func(urn resource.URN, id resource.ID, 371 inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) { 372 373 switch id { 374 case "0", "4": 375 // We want to delete resources A::0 and A::4. 376 return plugin.ReadResult{}, resource.StatusOK, nil 377 default: 378 return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil 379 } 380 }, 381 }, nil 382 }), 383 } 384 385 p.Options.Host = deploytest.NewPluginHost(nil, nil, nil, loaders...) 386 387 p.Steps = []TestStep{ 388 { 389 Op: Refresh, 390 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 391 _ []Event, res result.Result) result.Result { 392 393 // Should see only refreshes. 394 for _, entry := range entries { 395 if len(refreshTargets) > 0 { 396 // should only see changes to urns we explicitly asked to change 397 assert.Containsf(t, refreshTargets, entry.Step.URN(), 398 "Refreshed a resource that wasn't a target: %v", entry.Step.URN()) 399 } 400 401 assert.Equal(t, deploy.OpRefresh, entry.Step.Op()) 402 } 403 404 return res 405 }, 406 }, 407 } 408 409 snap := p.Run(t, old) 410 411 provURN := p.NewProviderURN("pkgA", "default", "") 412 413 // The new resources will have had their default provider urn filled in. We fill this in on 414 // the old resources here as well so that the equal checks below pass 415 setProviderRef(t, oldResources, snap.Resources, provURN) 416 417 for _, r := range snap.Resources { 418 switch urn := r.URN; urn { 419 case provURN: 420 continue 421 case urnA, urnB, urnC: 422 // break 423 default: 424 t.Fatalf("unexpected resource %v", urn) 425 } 426 427 if len(refreshTargets) == 0 || containsURN(refreshTargets, urnA) { 428 // 'A' was deleted, so we should see the impact downstream. 429 430 switch r.ID { 431 case "1": 432 // A::0 was deleted, so B's dependency list should be empty. 433 assert.Equal(t, urnB, r.URN) 434 assert.Empty(t, r.Dependencies) 435 case "2": 436 // A::0 was deleted, so C's dependency list should only contain B. 437 assert.Equal(t, urnC, r.URN) 438 assert.Equal(t, []resource.URN{urnB}, r.Dependencies) 439 case "3": 440 // A::3 should not have changed. 441 assert.Equal(t, oldResources[3], r) 442 case "5": 443 // A::4 was deleted but A::3 was still refernceable by C, so C should not have changed. 444 assert.Equal(t, oldResources[5], r) 445 default: 446 t.Fatalf("Unexpected changed resource when refreshing %v: %v::%v", refreshTargets, r.URN, r.ID) 447 } 448 } else { 449 // A was not deleted. So nothing should be impacted. 450 id, err := strconv.Atoi(r.ID.String()) 451 assert.NoError(t, err) 452 assert.Equal(t, oldResources[id], r) 453 } 454 } 455 } 456 457 func containsURN(urns []resource.URN, urn resource.URN) bool { 458 for _, val := range urns { 459 if val == urn { 460 return true 461 } 462 } 463 464 return false 465 } 466 467 // Tests basic refresh functionality. 468 func TestRefreshBasics(t *testing.T) { 469 t.Parallel() 470 471 names := []string{"resA", "resB", "resC"} 472 473 // Try refreshing a stack with every combination of the three above resources as a target to 474 // refresh. 475 subsets := combinations.All(names) 476 477 // combinations.All doesn't return the empty set. So explicitly test that case (i.e. test no 478 // targets specified) 479 //validateRefreshBasicsCombination(t, names, []string{}) 480 481 for _, subset := range subsets { 482 validateRefreshBasicsCombination(t, names, subset) 483 } 484 } 485 486 func validateRefreshBasicsCombination(t *testing.T, names []string, targets []string) { 487 p := &TestPlan{} 488 489 const resType = "pkgA:m:typA" 490 491 urnA := p.NewURN(resType, names[0], "") 492 urnB := p.NewURN(resType, names[1], "") 493 urnC := p.NewURN(resType, names[2], "") 494 urns := []resource.URN{urnA, urnB, urnC} 495 496 refreshTargets := []resource.URN{} 497 498 for _, target := range targets { 499 refreshTargets = append(p.Options.RefreshTargets.Literals(), pickURN(t, urns, names, target)) 500 } 501 502 p.Options.RefreshTargets = deploy.NewUrnTargetsFromUrns(refreshTargets) 503 504 newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State { 505 return &resource.State{ 506 Type: urn.Type(), 507 URN: urn, 508 Custom: true, 509 Delete: delete, 510 ID: id, 511 Inputs: resource.PropertyMap{}, 512 Outputs: resource.PropertyMap{}, 513 Dependencies: dependencies, 514 } 515 } 516 517 oldResources := []*resource.State{ 518 newResource(urnA, "0", false), 519 newResource(urnB, "1", false, urnA), 520 newResource(urnC, "2", false, urnA, urnB), 521 newResource(urnA, "3", true), 522 newResource(urnA, "4", true), 523 newResource(urnC, "5", true, urnA, urnB), 524 } 525 526 newStates := map[resource.ID]plugin.ReadResult{ 527 // A::0 and A::3 will have no changes. 528 "0": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}}, 529 "3": {Outputs: resource.PropertyMap{}, Inputs: resource.PropertyMap{}}, 530 531 // B::1 and A::4 will have changes. The latter will also have input changes. 532 "1": {Outputs: resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, Inputs: resource.PropertyMap{}}, 533 "4": { 534 Outputs: resource.PropertyMap{"baz": resource.NewStringProperty("qux")}, 535 Inputs: resource.PropertyMap{"oof": resource.NewStringProperty("zab")}, 536 }, 537 538 // C::2 and C::5 will be deleted. 539 "2": {}, 540 "5": {}, 541 } 542 543 old := &deploy.Snapshot{ 544 Resources: oldResources, 545 } 546 547 loaders := []*deploytest.ProviderLoader{ 548 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 549 return &deploytest.Provider{ 550 ReadF: func(urn resource.URN, id resource.ID, 551 inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) { 552 553 new, hasNewState := newStates[id] 554 assert.True(t, hasNewState) 555 return new, resource.StatusOK, nil 556 }, 557 }, nil 558 }), 559 } 560 561 p.Options.Host = deploytest.NewPluginHost(nil, nil, nil, loaders...) 562 563 p.Steps = []TestStep{{ 564 Op: Refresh, 565 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 566 _ []Event, res result.Result) result.Result { 567 568 // Should see only refreshes. 569 for _, entry := range entries { 570 if len(refreshTargets) > 0 { 571 // should only see changes to urns we explicitly asked to change 572 assert.Containsf(t, refreshTargets, entry.Step.URN(), 573 "Refreshed a resource that wasn't a target: %v", entry.Step.URN()) 574 } 575 576 assert.Equal(t, deploy.OpRefresh, entry.Step.Op()) 577 resultOp := entry.Step.(*deploy.RefreshStep).ResultOp() 578 579 old := entry.Step.Old() 580 if !old.Custom || providers.IsProviderType(old.Type) { 581 // Component and provider resources should never change. 582 assert.Equal(t, deploy.OpSame, resultOp) 583 continue 584 } 585 586 expected, new := newStates[old.ID], entry.Step.New() 587 if expected.Outputs == nil { 588 // If the resource was deleted, we want the result op to be an OpDelete. 589 assert.Nil(t, new) 590 assert.Equal(t, deploy.OpDelete, resultOp) 591 } else { 592 // If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want 593 // an OpSame. 594 if reflect.DeepEqual(old.Outputs, expected.Outputs) { 595 assert.Equal(t, deploy.OpSame, resultOp) 596 } else { 597 assert.Equal(t, deploy.OpUpdate, resultOp) 598 } 599 600 // Only the inputs and outputs should have changed (if anything changed). 601 old.Inputs = expected.Inputs 602 old.Outputs = expected.Outputs 603 assert.Equal(t, old, new) 604 } 605 } 606 return res 607 }, 608 }} 609 snap := p.Run(t, old) 610 611 provURN := p.NewProviderURN("pkgA", "default", "") 612 613 // The new resources will have had their default provider urn filled in. We fill this in on 614 // the old resources here as well so that the equal checks below pass 615 setProviderRef(t, oldResources, snap.Resources, provURN) 616 617 for _, r := range snap.Resources { 618 switch urn := r.URN; urn { 619 case provURN: 620 continue 621 case urnA, urnB, urnC: 622 // break 623 default: 624 t.Fatalf("unexpected resource %v", urn) 625 } 626 627 // The only resources left in the checkpoint should be those that were not deleted by the refresh. 628 expected := newStates[r.ID] 629 assert.NotNil(t, expected) 630 631 idx, err := strconv.ParseInt(string(r.ID), 0, 0) 632 assert.NoError(t, err) 633 634 targetedForRefresh := false 635 for _, targetUrn := range refreshTargets { 636 if targetUrn == r.URN { 637 targetedForRefresh = true 638 } 639 } 640 641 // If targeted for refresh the new resources should be equal to the old resources + the new inputs and outputs 642 old := oldResources[int(idx)] 643 if targetedForRefresh { 644 old.Inputs = expected.Inputs 645 old.Outputs = expected.Outputs 646 } 647 assert.Equal(t, old, r) 648 } 649 } 650 651 // Tests that an interrupted refresh leaves behind an expected state. 652 func TestCanceledRefresh(t *testing.T) { 653 t.Parallel() 654 655 p := &TestPlan{} 656 657 const resType = "pkgA:m:typA" 658 659 urnA := p.NewURN(resType, "resA", "") 660 urnB := p.NewURN(resType, "resB", "") 661 urnC := p.NewURN(resType, "resC", "") 662 663 newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State { 664 return &resource.State{ 665 Type: urn.Type(), 666 URN: urn, 667 Custom: true, 668 Delete: delete, 669 ID: id, 670 Inputs: resource.PropertyMap{}, 671 Outputs: resource.PropertyMap{}, 672 Dependencies: dependencies, 673 } 674 } 675 676 oldResources := []*resource.State{ 677 newResource(urnA, "0", false), 678 newResource(urnB, "1", false), 679 newResource(urnC, "2", false), 680 } 681 682 newStates := map[resource.ID]resource.PropertyMap{ 683 // A::0 and B::1 will have changes; D::3 will be deleted. 684 "0": {"foo": resource.NewStringProperty("bar")}, 685 "1": {"baz": resource.NewStringProperty("qux")}, 686 "2": nil, 687 } 688 689 old := &deploy.Snapshot{ 690 Resources: oldResources, 691 } 692 693 // Set up a cancelable context for the refresh operation. 694 ctx, cancel := context.WithCancel(context.Background()) 695 696 // Serialize all refreshes s.t. we can cancel after the first is issued. 697 refreshes, cancelled := make(chan resource.ID), make(chan bool) 698 go func() { 699 <-refreshes 700 cancel() 701 }() 702 703 loaders := []*deploytest.ProviderLoader{ 704 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 705 return &deploytest.Provider{ 706 ReadF: func(urn resource.URN, id resource.ID, 707 inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) { 708 709 refreshes <- id 710 <-cancelled 711 712 new, hasNewState := newStates[id] 713 assert.True(t, hasNewState) 714 return plugin.ReadResult{Outputs: new}, resource.StatusOK, nil 715 }, 716 CancelF: func() error { 717 close(cancelled) 718 return nil 719 }, 720 }, nil 721 }), 722 } 723 724 refreshed := make(map[resource.ID]bool) 725 op := TestOp(Refresh) 726 options := UpdateOptions{ 727 Parallel: 1, 728 Host: deploytest.NewPluginHost(nil, nil, nil, loaders...), 729 } 730 project, target := p.GetProject(), p.GetTarget(t, old) 731 validate := func(project workspace.Project, target deploy.Target, entries JournalEntries, 732 _ []Event, res result.Result) result.Result { 733 734 for _, entry := range entries { 735 assert.Equal(t, deploy.OpRefresh, entry.Step.Op()) 736 resultOp := entry.Step.(*deploy.RefreshStep).ResultOp() 737 738 old := entry.Step.Old() 739 if !old.Custom || providers.IsProviderType(old.Type) { 740 // Component and provider resources should never change. 741 assert.Equal(t, deploy.OpSame, resultOp) 742 continue 743 } 744 745 refreshed[old.ID] = true 746 747 expected, new := newStates[old.ID], entry.Step.New() 748 if expected == nil { 749 // If the resource was deleted, we want the result op to be an OpDelete. 750 assert.Nil(t, new) 751 assert.Equal(t, deploy.OpDelete, resultOp) 752 } else { 753 // If there were changes to the outputs, we want the result op to be an OpUpdate. Otherwise we want 754 // an OpSame. 755 if reflect.DeepEqual(old.Outputs, expected) { 756 assert.Equal(t, deploy.OpSame, resultOp) 757 } else { 758 assert.Equal(t, deploy.OpUpdate, resultOp) 759 } 760 761 // Only the outputs should have changed (if anything changed). 762 old.Outputs = expected 763 assert.Equal(t, old, new) 764 } 765 } 766 return res 767 } 768 769 snap, res := op.RunWithContext(ctx, project, target, options, false, nil, validate) 770 assertIsErrorOrBailResult(t, res) 771 assert.Equal(t, 1, len(refreshed)) 772 773 provURN := p.NewProviderURN("pkgA", "default", "") 774 775 // The new resources will have had their default provider urn filled in. We fill this in on 776 // the old resources here as well so that the equal checks below pass 777 setProviderRef(t, oldResources, snap.Resources, provURN) 778 779 for _, r := range snap.Resources { 780 switch urn := r.URN; urn { 781 case provURN: 782 continue 783 case urnA, urnB, urnC: 784 // break 785 default: 786 t.Fatalf("unexpected resource %v", urn) 787 } 788 789 idx, err := strconv.ParseInt(string(r.ID), 0, 0) 790 assert.NoError(t, err) 791 792 if refreshed[r.ID] { 793 // The refreshed resource should have its new state. 794 expected := newStates[r.ID] 795 if expected == nil { 796 assert.Fail(t, "refreshed resource was not deleted") 797 } else { 798 old := oldResources[int(idx)] 799 old.Outputs = expected 800 assert.Equal(t, old, r) 801 } 802 } else { 803 // Any resources that were not refreshed should retain their original state. 804 old := oldResources[int(idx)] 805 assert.Equal(t, old, r) 806 } 807 } 808 } 809 810 func TestRefreshStepWillPersistUpdatedIDs(t *testing.T) { 811 t.Parallel() 812 813 p := &TestPlan{} 814 815 provURN := p.NewProviderURN("pkgA", "default", "") 816 resURN := p.NewURN("pkgA:m:typA", "resA", "") 817 idBefore := resource.ID("myid") 818 idAfter := resource.ID("mynewid") 819 outputs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")} 820 821 loaders := []*deploytest.ProviderLoader{ 822 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 823 return &deploytest.Provider{ 824 ReadF: func( 825 urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, 826 ) (plugin.ReadResult, resource.Status, error) { 827 return plugin.ReadResult{ID: idAfter, Outputs: outputs, Inputs: resource.PropertyMap{}}, resource.StatusOK, nil 828 }, 829 }, nil 830 }), 831 } 832 833 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 834 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 835 assert.NoError(t, err) 836 return nil 837 }) 838 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 839 840 p.Options.Host = host 841 842 old := &deploy.Snapshot{ 843 Resources: []*resource.State{ 844 { 845 Type: resURN.Type(), 846 URN: resURN, 847 Custom: true, 848 ID: idBefore, 849 Inputs: resource.PropertyMap{}, 850 Outputs: outputs, 851 InitErrors: []string{"Resource failed to initialize"}, 852 }, 853 }, 854 } 855 856 p.Steps = []TestStep{{Op: Refresh, SkipPreview: true}} 857 snap := p.Run(t, old) 858 859 for _, resource := range snap.Resources { 860 switch urn := resource.URN; urn { 861 case provURN: 862 // break 863 case resURN: 864 assert.Empty(t, resource.InitErrors) 865 assert.Equal(t, idAfter, resource.ID) 866 default: 867 t.Fatalf("unexpected resource %v", urn) 868 } 869 } 870 } 871 872 // TestRefreshUpdateWithDeletedResource validates that the engine handles a deleted resource without error on an 873 // update with refresh. 874 func TestRefreshUpdateWithDeletedResource(t *testing.T) { 875 t.Parallel() 876 877 p := &TestPlan{} 878 879 resURN := p.NewURN("pkgA:m:typA", "resA", "") 880 idBefore := resource.ID("myid") 881 882 loaders := []*deploytest.ProviderLoader{ 883 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 884 return &deploytest.Provider{ 885 ReadF: func( 886 urn resource.URN, id resource.ID, inputs, state resource.PropertyMap, 887 ) (plugin.ReadResult, resource.Status, error) { 888 return plugin.ReadResult{}, resource.StatusOK, nil 889 }, 890 }, nil 891 }), 892 } 893 894 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 895 return nil 896 }) 897 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 898 899 p.Options.Host = host 900 p.Options.Refresh = true 901 902 old := &deploy.Snapshot{ 903 Resources: []*resource.State{ 904 { 905 Type: resURN.Type(), 906 URN: resURN, 907 Custom: true, 908 ID: idBefore, 909 Inputs: resource.PropertyMap{}, 910 Outputs: resource.PropertyMap{}, 911 }, 912 }, 913 } 914 915 p.Steps = []TestStep{{Op: Update}} 916 snap := p.Run(t, old) 917 assert.Equal(t, 0, len(snap.Resources)) 918 }