github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/engine/lifecycletest/pulumi_test.go (about) 1 // Copyright 2016-2022, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // nolint: goconst 16 package lifecycletest 17 18 import ( 19 "context" 20 "errors" 21 "flag" 22 "fmt" 23 "os" 24 "regexp" 25 "strings" 26 "sync" 27 "testing" 28 29 "github.com/blang/semver" 30 pbempty "github.com/golang/protobuf/ptypes/empty" 31 32 "github.com/stretchr/testify/assert" 33 "github.com/stretchr/testify/require" 34 "google.golang.org/grpc" 35 "google.golang.org/grpc/codes" 36 "google.golang.org/grpc/status" 37 38 "github.com/pulumi/pulumi/pkg/v3/engine" 39 . "github.com/pulumi/pulumi/pkg/v3/engine" 40 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 41 "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" 42 "github.com/pulumi/pulumi/sdk/v3/go/common/diag" 43 "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" 44 "github.com/pulumi/pulumi/sdk/v3/go/common/display" 45 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 46 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" 47 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" 48 "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" 49 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 50 "github.com/pulumi/pulumi/sdk/v3/go/common/util/result" 51 "github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil" 52 "github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil/rpcerror" 53 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 54 pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" 55 ) 56 57 func SuccessfulSteps(entries JournalEntries) []deploy.Step { 58 var steps []deploy.Step 59 for _, entry := range entries { 60 if entry.Kind == JournalEntrySuccess { 61 steps = append(steps, entry.Step) 62 } 63 } 64 return steps 65 } 66 67 type StepSummary struct { 68 Op display.StepOp 69 URN resource.URN 70 } 71 72 func AssertSameSteps(t *testing.T, expected []StepSummary, actual []deploy.Step) bool { 73 assert.Equal(t, len(expected), len(actual)) 74 for _, exp := range expected { 75 act := actual[0] 76 actual = actual[1:] 77 78 if !assert.Equal(t, exp.Op, act.Op()) || !assert.Equal(t, exp.URN, act.URN()) { 79 return false 80 } 81 } 82 return true 83 } 84 85 func ExpectDiagMessage(t *testing.T, messagePattern string) ValidateFunc { 86 validate := func( 87 project workspace.Project, target deploy.Target, 88 entries JournalEntries, events []Event, 89 res result.Result) result.Result { 90 91 assert.NotNil(t, res) 92 93 for i := range events { 94 if events[i].Type == "diag" { 95 payload := events[i].Payload().(engine.DiagEventPayload) 96 match, err := regexp.MatchString(messagePattern, payload.Message) 97 if err != nil { 98 return result.FromError(err) 99 } 100 if match { 101 return nil 102 } 103 return result.Errorf("Unexpected diag message: %s", payload.Message) 104 } 105 } 106 return result.Error("Expected a diagnostic message, got none") 107 } 108 return validate 109 } 110 111 func pickURN(t *testing.T, urns []resource.URN, names []string, target string) resource.URN { 112 assert.Equal(t, len(urns), len(names)) 113 assert.Contains(t, names, target) 114 115 for i, name := range names { 116 if name == target { 117 return urns[i] 118 } 119 } 120 121 t.Fatalf("Could not find target: %v in %v", target, names) 122 return "" 123 } 124 125 func TestMain(m *testing.M) { 126 grpcDefault := flag.Bool("grpc-plugins", false, "enable or disable gRPC providers by default") 127 128 flag.Parse() 129 130 if *grpcDefault { 131 deploytest.UseGrpcPluginsByDefault = true 132 } 133 134 os.Exit(m.Run()) 135 } 136 137 func TestEmptyProgramLifecycle(t *testing.T) { 138 t.Parallel() 139 140 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error { 141 return nil 142 }) 143 host := deploytest.NewPluginHost(nil, nil, program) 144 145 p := &TestPlan{ 146 Options: UpdateOptions{Host: host}, 147 Steps: MakeBasicLifecycleSteps(t, 0), 148 } 149 p.Run(t, nil) 150 } 151 152 func TestSingleResourceDiffUnavailable(t *testing.T) { 153 t.Parallel() 154 155 loaders := []*deploytest.ProviderLoader{ 156 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 157 return &deploytest.Provider{ 158 DiffF: func(urn resource.URN, id resource.ID, 159 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 160 161 return plugin.DiffResult{}, plugin.DiffUnavailable("diff unavailable") 162 }, 163 }, nil 164 }), 165 } 166 167 inputs := resource.PropertyMap{} 168 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 169 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 170 Inputs: inputs, 171 }) 172 assert.NoError(t, err) 173 return nil 174 }) 175 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 176 177 p := &TestPlan{ 178 Options: UpdateOptions{Host: host}, 179 } 180 resURN := p.NewURN("pkgA:m:typA", "resA", "") 181 182 // Run the initial update. 183 project := p.GetProject() 184 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 185 assert.Nil(t, res) 186 187 // Now run a preview. Expect a warning because the diff is unavailable. 188 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient, 189 func(_ workspace.Project, _ deploy.Target, _ JournalEntries, 190 events []Event, res result.Result) result.Result { 191 192 found := false 193 for _, e := range events { 194 if e.Type == DiagEvent { 195 p := e.Payload().(DiagEventPayload) 196 if p.URN == resURN && p.Severity == diag.Warning && p.Message == "<{%reset%}>diff unavailable<{%reset%}>\n" { 197 found = true 198 break 199 } 200 } 201 } 202 assert.True(t, found) 203 return res 204 }) 205 assert.Nil(t, res) 206 } 207 208 // Test that ensures that we log diagnostics for resources that receive an error from Check. (Note that this 209 // is distinct from receiving non-error failures from Check.) 210 func TestCheckFailureRecord(t *testing.T) { 211 t.Parallel() 212 213 loaders := []*deploytest.ProviderLoader{ 214 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 215 return &deploytest.Provider{ 216 CheckF: func(urn resource.URN, 217 olds, news resource.PropertyMap, randomSeed []byte) (resource.PropertyMap, []plugin.CheckFailure, error) { 218 return nil, nil, errors.New("oh no, check had an error") 219 }, 220 }, nil 221 }), 222 } 223 224 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 225 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 226 assert.Error(t, err) 227 return err 228 }) 229 230 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 231 p := &TestPlan{ 232 Options: UpdateOptions{Host: host}, 233 Steps: []TestStep{{ 234 Op: Update, 235 ExpectFailure: true, 236 SkipPreview: true, 237 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 238 evts []Event, res result.Result) result.Result { 239 240 sawFailure := false 241 for _, evt := range evts { 242 if evt.Type == DiagEvent { 243 e := evt.Payload().(DiagEventPayload) 244 msg := colors.Never.Colorize(e.Message) 245 sawFailure = msg == "oh no, check had an error\n" && e.Severity == diag.Error 246 } 247 } 248 249 assert.True(t, sawFailure) 250 return res 251 }, 252 }}, 253 } 254 255 p.Run(t, nil) 256 } 257 258 // Test that checks that we emit diagnostics for properties that check says are invalid. 259 func TestCheckFailureInvalidPropertyRecord(t *testing.T) { 260 t.Parallel() 261 262 loaders := []*deploytest.ProviderLoader{ 263 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 264 return &deploytest.Provider{ 265 CheckF: func(urn resource.URN, 266 olds, news resource.PropertyMap, randomSeed []byte) (resource.PropertyMap, []plugin.CheckFailure, error) { 267 return nil, []plugin.CheckFailure{{ 268 Property: "someprop", 269 Reason: "field is not valid", 270 }}, nil 271 }, 272 }, nil 273 }), 274 } 275 276 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 277 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 278 assert.Error(t, err) 279 return err 280 }) 281 282 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 283 p := &TestPlan{ 284 Options: UpdateOptions{Host: host}, 285 Steps: []TestStep{{ 286 Op: Update, 287 ExpectFailure: true, 288 SkipPreview: true, 289 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 290 evts []Event, res result.Result) result.Result { 291 292 sawFailure := false 293 for _, evt := range evts { 294 if evt.Type == DiagEvent { 295 e := evt.Payload().(DiagEventPayload) 296 msg := colors.Never.Colorize(e.Message) 297 sawFailure = strings.Contains(msg, "field is not valid") && e.Severity == diag.Error 298 if sawFailure { 299 break 300 } 301 } 302 } 303 304 assert.True(t, sawFailure) 305 return res 306 }, 307 }}, 308 } 309 310 p.Run(t, nil) 311 312 } 313 314 // Tests that errors returned directly from the language host get logged by the engine. 315 func TestLanguageHostDiagnostics(t *testing.T) { 316 t.Parallel() 317 318 loaders := []*deploytest.ProviderLoader{ 319 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 320 return &deploytest.Provider{}, nil 321 }), 322 } 323 324 errorText := "oh no" 325 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error { 326 // Exiting immediately with an error simulates a language exiting immediately with a non-zero exit code. 327 return errors.New(errorText) 328 }) 329 330 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 331 p := &TestPlan{ 332 Options: UpdateOptions{Host: host}, 333 Steps: []TestStep{{ 334 Op: Update, 335 ExpectFailure: true, 336 SkipPreview: true, 337 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 338 evts []Event, res result.Result) result.Result { 339 340 assertIsErrorOrBailResult(t, res) 341 sawExitCode := false 342 for _, evt := range evts { 343 if evt.Type == DiagEvent { 344 e := evt.Payload().(DiagEventPayload) 345 msg := colors.Never.Colorize(e.Message) 346 sawExitCode = strings.Contains(msg, errorText) && e.Severity == diag.Error 347 if sawExitCode { 348 break 349 } 350 } 351 } 352 353 assert.True(t, sawExitCode) 354 return res 355 }, 356 }}, 357 } 358 359 p.Run(t, nil) 360 } 361 362 type brokenDecrypter struct { 363 ErrorMessage string 364 } 365 366 func (b brokenDecrypter) DecryptValue(_ context.Context, _ string) (string, error) { 367 return "", fmt.Errorf(b.ErrorMessage) 368 } 369 370 func (b brokenDecrypter) BulkDecrypt(_ context.Context, _ []string) (map[string]string, error) { 371 return nil, fmt.Errorf(b.ErrorMessage) 372 } 373 374 // Tests that the engine presents a reasonable error message when a decrypter fails to decrypt a config value. 375 func TestBrokenDecrypter(t *testing.T) { 376 t.Parallel() 377 378 loaders := []*deploytest.ProviderLoader{ 379 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 380 return &deploytest.Provider{}, nil 381 }), 382 } 383 384 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, _ *deploytest.ResourceMonitor) error { 385 return nil 386 }) 387 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 388 key := config.MustMakeKey("foo", "bar") 389 msg := "decryption failed" 390 configMap := make(config.Map) 391 configMap[key] = config.NewSecureValue("hunter2") 392 p := &TestPlan{ 393 Options: UpdateOptions{Host: host}, 394 Decrypter: brokenDecrypter{ErrorMessage: msg}, 395 Config: configMap, 396 Steps: []TestStep{{ 397 Op: Update, 398 ExpectFailure: true, 399 SkipPreview: true, 400 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 401 evts []Event, res result.Result) result.Result { 402 403 assertIsErrorOrBailResult(t, res) 404 decryptErr := res.Error().(DecryptError) 405 assert.Equal(t, key, decryptErr.Key) 406 assert.Contains(t, decryptErr.Err.Error(), msg) 407 return res 408 }, 409 }}, 410 } 411 412 p.Run(t, nil) 413 } 414 415 func TestBadResourceType(t *testing.T) { 416 t.Parallel() 417 418 loaders := []*deploytest.ProviderLoader{ 419 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 420 return &deploytest.Provider{}, nil 421 }), 422 } 423 424 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, mon *deploytest.ResourceMonitor) error { 425 _, _, _, err := mon.RegisterResource("very:bad", "resA", true) 426 assert.Error(t, err) 427 rpcerr, ok := rpcerror.FromError(err) 428 assert.True(t, ok) 429 assert.Equal(t, codes.InvalidArgument, rpcerr.Code()) 430 assert.Contains(t, rpcerr.Message(), "Type 'very:bad' is not a valid type token") 431 432 _, _, err = mon.ReadResource("very:bad", "someResource", "someId", "", resource.PropertyMap{}, "", "") 433 assert.Error(t, err) 434 rpcerr, ok = rpcerror.FromError(err) 435 assert.True(t, ok) 436 assert.Equal(t, codes.InvalidArgument, rpcerr.Code()) 437 assert.Contains(t, rpcerr.Message(), "Type 'very:bad' is not a valid type token") 438 439 // Component resources may have any format type. 440 _, _, _, noErr := mon.RegisterResource("a:component", "resB", false) 441 assert.NoError(t, noErr) 442 443 _, _, _, noErr = mon.RegisterResource("singlename", "resC", false) 444 assert.NoError(t, noErr) 445 446 return err 447 }) 448 449 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 450 p := &TestPlan{ 451 Options: UpdateOptions{Host: host}, 452 Steps: []TestStep{{ 453 Op: Update, 454 ExpectFailure: true, 455 SkipPreview: true, 456 }}, 457 } 458 459 p.Run(t, nil) 460 } 461 462 // Tests that provider cancellation occurs as expected. 463 func TestProviderCancellation(t *testing.T) { 464 t.Parallel() 465 466 const resourceCount = 4 467 468 // Set up a cancelable context for the refresh operation. 469 ctx, cancel := context.WithCancel(context.Background()) 470 471 // Wait for our resource ops, then cancel. 472 var ops sync.WaitGroup 473 ops.Add(resourceCount) 474 go func() { 475 ops.Wait() 476 cancel() 477 }() 478 479 // Set up an independent cancelable context for the provider's operations. 480 provCtx, provCancel := context.WithCancel(context.Background()) 481 loaders := []*deploytest.ProviderLoader{ 482 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 483 return &deploytest.Provider{ 484 CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64, 485 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 486 487 // Inform the waiter that we've entered a provider op and wait for cancellation. 488 ops.Done() 489 <-provCtx.Done() 490 491 return resource.ID(urn.Name()), resource.PropertyMap{}, resource.StatusOK, nil 492 }, 493 CancelF: func() error { 494 provCancel() 495 return nil 496 }, 497 }, nil 498 }), 499 } 500 501 done := make(chan bool) 502 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 503 errors := make([]error, resourceCount) 504 var resources sync.WaitGroup 505 resources.Add(resourceCount) 506 for i := 0; i < resourceCount; i++ { 507 go func(idx int) { 508 _, _, _, errors[idx] = monitor.RegisterResource("pkgA:m:typA", fmt.Sprintf("res%d", idx), true) 509 resources.Done() 510 }(i) 511 } 512 resources.Wait() 513 for _, err := range errors { 514 assert.NoError(t, err) 515 } 516 close(done) 517 return nil 518 }) 519 520 p := &TestPlan{} 521 op := TestOp(Update) 522 options := UpdateOptions{ 523 Parallel: resourceCount, 524 Host: deploytest.NewPluginHost(nil, nil, program, loaders...), 525 } 526 project, target := p.GetProject(), p.GetTarget(t, nil) 527 528 _, res := op.RunWithContext(ctx, project, target, options, false, nil, nil) 529 assertIsErrorOrBailResult(t, res) 530 531 // Wait for the program to finish. 532 <-done 533 } 534 535 // Tests that a preview works for a stack with pending operations. 536 func TestPreviewWithPendingOperations(t *testing.T) { 537 t.Parallel() 538 539 p := &TestPlan{} 540 541 const resType = "pkgA:m:typA" 542 urnA := p.NewURN(resType, "resA", "") 543 544 newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State { 545 return &resource.State{ 546 Type: urn.Type(), 547 URN: urn, 548 Custom: true, 549 Delete: delete, 550 ID: id, 551 Inputs: resource.PropertyMap{}, 552 Outputs: resource.PropertyMap{}, 553 Dependencies: dependencies, 554 } 555 } 556 557 old := &deploy.Snapshot{ 558 PendingOperations: []resource.Operation{{ 559 Resource: newResource(urnA, "0", false), 560 Type: resource.OperationTypeUpdating, 561 }}, 562 Resources: []*resource.State{ 563 newResource(urnA, "0", false), 564 }, 565 } 566 567 loaders := []*deploytest.ProviderLoader{ 568 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 569 return &deploytest.Provider{}, nil 570 }), 571 } 572 573 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 574 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 575 assert.NoError(t, err) 576 return nil 577 }) 578 579 op := TestOp(Update) 580 options := UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)} 581 project, target := p.GetProject(), p.GetTarget(t, old) 582 583 // A preview should succeed despite the pending operations. 584 _, res := op.Run(project, target, options, true, nil, nil) 585 assert.Nil(t, res) 586 } 587 588 // Tests that a refresh works for a stack with pending operations. 589 func TestRefreshWithPendingOperations(t *testing.T) { 590 t.Parallel() 591 592 p := &TestPlan{} 593 594 const resType = "pkgA:m:typA" 595 urnA := p.NewURN(resType, "resA", "") 596 597 newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State { 598 return &resource.State{ 599 Type: urn.Type(), 600 URN: urn, 601 Custom: true, 602 Delete: delete, 603 ID: id, 604 Inputs: resource.PropertyMap{}, 605 Outputs: resource.PropertyMap{}, 606 Dependencies: dependencies, 607 } 608 } 609 610 old := &deploy.Snapshot{ 611 PendingOperations: []resource.Operation{{ 612 Resource: newResource(urnA, "0", false), 613 Type: resource.OperationTypeUpdating, 614 }}, 615 Resources: []*resource.State{ 616 newResource(urnA, "0", false), 617 }, 618 } 619 620 loaders := []*deploytest.ProviderLoader{ 621 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 622 return &deploytest.Provider{}, nil 623 }), 624 } 625 626 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 627 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 628 assert.NoError(t, err) 629 return nil 630 }) 631 632 op := TestOp(Update) 633 options := UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)} 634 project, target := p.GetProject(), p.GetTarget(t, old) 635 636 // With a refresh, the update should succeed. 637 withRefresh := options 638 withRefresh.Refresh = true 639 new, res := op.Run(project, target, withRefresh, false, nil, nil) 640 assert.Nil(t, res) 641 assert.Len(t, new.PendingOperations, 0) 642 643 // Similarly, the update should succeed if performed after a separate refresh. 644 new, res = TestOp(Refresh).Run(project, target, options, false, nil, nil) 645 assert.Nil(t, res) 646 assert.Len(t, new.PendingOperations, 0) 647 648 _, res = op.Run(project, p.GetTarget(t, new), options, false, nil, nil) 649 assert.Nil(t, res) 650 } 651 652 // Test to make sure that if we pulumi refresh 653 // while having pending CREATE operations, 654 // that these are preserved after the refresh. 655 func TestRefreshPreservesPendingCreateOperations(t *testing.T) { 656 t.Parallel() 657 658 p := &TestPlan{} 659 660 const resType = "pkgA:m:typA" 661 urnA := p.NewURN(resType, "resA", "") 662 urnB := p.NewURN(resType, "resB", "") 663 664 newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State { 665 return &resource.State{ 666 Type: urn.Type(), 667 URN: urn, 668 Custom: true, 669 Delete: delete, 670 ID: id, 671 Inputs: resource.PropertyMap{}, 672 Outputs: resource.PropertyMap{}, 673 Dependencies: dependencies, 674 } 675 } 676 677 // Notice here, we have two pending operations: update and create 678 // After a refresh, only the pending CREATE operation should 679 // be in the updated snapshot 680 resA := newResource(urnA, "0", false) 681 resB := newResource(urnB, "0", false) 682 old := &deploy.Snapshot{ 683 PendingOperations: []resource.Operation{ 684 { 685 Resource: resA, 686 Type: resource.OperationTypeUpdating, 687 }, 688 { 689 Resource: resB, 690 Type: resource.OperationTypeCreating, 691 }, 692 }, 693 Resources: []*resource.State{ 694 resA, 695 }, 696 } 697 698 loaders := []*deploytest.ProviderLoader{ 699 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 700 return &deploytest.Provider{}, nil 701 }), 702 } 703 704 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 705 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 706 assert.NoError(t, err) 707 return nil 708 }) 709 710 op := TestOp(Update) 711 options := UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)} 712 project, target := p.GetProject(), p.GetTarget(t, old) 713 714 // With a refresh, the update should succeed. 715 withRefresh := options 716 withRefresh.Refresh = true 717 new, res := op.Run(project, target, withRefresh, false, nil, nil) 718 assert.Nil(t, res) 719 // Assert that pending CREATE operation was preserved 720 assert.Len(t, new.PendingOperations, 1) 721 assert.Equal(t, resource.OperationTypeCreating, new.PendingOperations[0].Type) 722 assert.Equal(t, urnB, new.PendingOperations[0].Resource.URN) 723 } 724 725 func findPendingOperationsByType(opType resource.OperationType, snapshot *deploy.Snapshot) []resource.Operation { 726 var operations []resource.Operation 727 for _, operation := range snapshot.PendingOperations { 728 if operation.Type == opType { 729 operations = append(operations, operation) 730 } 731 } 732 return operations 733 } 734 735 // Update succeeds but gives a warning when there are pending operations 736 func TestUpdateShowsWarningWithPendingOperations(t *testing.T) { 737 t.Parallel() 738 739 p := &TestPlan{} 740 741 const resType = "pkgA:m:typA" 742 urnA := p.NewURN(resType, "resA", "") 743 urnB := p.NewURN(resType, "resB", "") 744 745 newResource := func(urn resource.URN, id resource.ID, delete bool, dependencies ...resource.URN) *resource.State { 746 return &resource.State{ 747 Type: urn.Type(), 748 URN: urn, 749 Custom: true, 750 Delete: delete, 751 ID: id, 752 Inputs: resource.PropertyMap{}, 753 Outputs: resource.PropertyMap{}, 754 Dependencies: dependencies, 755 } 756 } 757 758 old := &deploy.Snapshot{ 759 PendingOperations: []resource.Operation{ 760 { 761 Resource: newResource(urnA, "0", false), 762 Type: resource.OperationTypeUpdating, 763 }, 764 { 765 Resource: newResource(urnB, "1", false), 766 Type: resource.OperationTypeCreating, 767 }}, 768 Resources: []*resource.State{ 769 newResource(urnA, "0", false), 770 }, 771 } 772 773 loaders := []*deploytest.ProviderLoader{ 774 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 775 return &deploytest.Provider{}, nil 776 }), 777 } 778 779 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 780 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 781 assert.NoError(t, err) 782 return nil 783 }) 784 785 op := TestOp(Update) 786 options := UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)} 787 project, target := p.GetProject(), p.GetTarget(t, old) 788 789 // The update should succeed but give a warning 790 initialPartOfMessage := "Attempting to deploy or update resources with 1 pending operations from previous deployment." 791 validate := func( 792 project workspace.Project, target deploy.Target, 793 entries JournalEntries, events []Event, 794 res result.Result) result.Result { 795 for i := range events { 796 if events[i].Type == "diag" { 797 payload := events[i].Payload().(engine.DiagEventPayload) 798 799 if payload.Severity == "warning" && strings.Contains(payload.Message, initialPartOfMessage) { 800 return nil 801 } 802 return result.Errorf("Unexpected warning diag message: %s", payload.Message) 803 } 804 } 805 return result.Error("Expected a diagnostic message, got none") 806 } 807 808 new, _ := op.Run(project, target, options, false, nil, validate) 809 assert.NotNil(t, new) 810 811 assert.Equal(t, resource.OperationTypeCreating, new.PendingOperations[0].Type) 812 813 // Assert that CREATE pending operations are retained 814 // TODO: should revisit whether non-CREATE pending operations should also be retained 815 assert.Equal(t, 1, len(new.PendingOperations)) 816 createOperations := findPendingOperationsByType(resource.OperationTypeCreating, new) 817 assert.Equal(t, 1, len(createOperations)) 818 assert.Equal(t, urnB, createOperations[0].Resource.URN) 819 } 820 821 // Tests that a failed partial update causes the engine to persist the resource's old inputs and new outputs. 822 func TestUpdatePartialFailure(t *testing.T) { 823 t.Parallel() 824 825 loaders := []*deploytest.ProviderLoader{ 826 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 827 return &deploytest.Provider{ 828 DiffF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, 829 ignoreChanges []string) (plugin.DiffResult, error) { 830 831 return plugin.DiffResult{ 832 Changes: plugin.DiffSome, 833 }, nil 834 }, 835 836 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 837 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 838 839 outputs := resource.NewPropertyMapFromMap(map[string]interface{}{ 840 "output_prop": 42, 841 }) 842 843 return outputs, resource.StatusPartialFailure, errors.New("update failed to apply") 844 }, 845 }, nil 846 }), 847 } 848 849 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, mon *deploytest.ResourceMonitor) error { 850 _, _, _, err := mon.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 851 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 852 "input_prop": "new inputs", 853 }), 854 }) 855 return err 856 }) 857 858 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 859 p := &TestPlan{Options: UpdateOptions{Host: host}} 860 861 resURN := p.NewURN("pkgA:m:typA", "resA", "") 862 p.Steps = []TestStep{{ 863 Op: Update, 864 ExpectFailure: true, 865 SkipPreview: true, 866 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 867 evts []Event, res result.Result) result.Result { 868 869 assertIsErrorOrBailResult(t, res) 870 for _, entry := range entries { 871 switch urn := entry.Step.URN(); urn { 872 case resURN: 873 assert.Equal(t, deploy.OpUpdate, entry.Step.Op()) 874 switch entry.Kind { 875 case JournalEntryBegin: 876 continue 877 case JournalEntrySuccess: 878 inputs := entry.Step.New().Inputs 879 outputs := entry.Step.New().Outputs 880 assert.Len(t, inputs, 1) 881 assert.Len(t, outputs, 1) 882 assert.Equal(t, 883 resource.NewStringProperty("old inputs"), inputs[resource.PropertyKey("input_prop")]) 884 assert.Equal(t, 885 resource.NewNumberProperty(42), outputs[resource.PropertyKey("output_prop")]) 886 default: 887 t.Fatalf("unexpected journal operation: %d", entry.Kind) 888 } 889 } 890 } 891 892 return res 893 }, 894 }} 895 896 old := &deploy.Snapshot{ 897 Resources: []*resource.State{ 898 { 899 Type: resURN.Type(), 900 URN: resURN, 901 Custom: true, 902 ID: "1", 903 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 904 "input_prop": "old inputs", 905 }), 906 Outputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 907 "output_prop": 1, 908 }), 909 }, 910 }, 911 } 912 913 p.Run(t, old) 914 } 915 916 // Tests that the StackReference resource works as intended, 917 func TestStackReference(t *testing.T) { 918 t.Parallel() 919 920 loaders := []*deploytest.ProviderLoader{} 921 922 // Test that the normal lifecycle works correctly. 923 program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error { 924 _, _, state, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{ 925 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 926 "name": "other", 927 }), 928 }) 929 assert.NoError(t, err) 930 if !info.DryRun { 931 assert.Equal(t, "bar", state["outputs"].ObjectValue()["foo"].StringValue()) 932 } 933 return nil 934 }) 935 p := &TestPlan{ 936 BackendClient: &deploytest.BackendClient{ 937 GetStackOutputsF: func(ctx context.Context, name string) (resource.PropertyMap, error) { 938 switch name { 939 case "other": 940 return resource.NewPropertyMapFromMap(map[string]interface{}{ 941 "foo": "bar", 942 }), nil 943 default: 944 return nil, fmt.Errorf("unknown stack \"%s\"", name) 945 } 946 }, 947 }, 948 Options: UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)}, 949 Steps: MakeBasicLifecycleSteps(t, 2), 950 } 951 p.Run(t, nil) 952 953 // Test that changes to `name` cause replacement. 954 resURN := p.NewURN("pulumi:pulumi:StackReference", "other", "") 955 old := &deploy.Snapshot{ 956 Resources: []*resource.State{ 957 { 958 Type: resURN.Type(), 959 URN: resURN, 960 Custom: true, 961 ID: "1", 962 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 963 "name": "other2", 964 }), 965 Outputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 966 "name": "other2", 967 "outputs": resource.PropertyMap{}, 968 }), 969 }, 970 }, 971 } 972 p.Steps = []TestStep{{ 973 Op: Update, 974 SkipPreview: true, 975 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 976 evts []Event, res result.Result) result.Result { 977 978 assert.Nil(t, res) 979 for _, entry := range entries { 980 switch urn := entry.Step.URN(); urn { 981 case resURN: 982 switch entry.Step.Op() { 983 case deploy.OpCreateReplacement, deploy.OpDeleteReplaced, deploy.OpReplace: 984 // OK 985 default: 986 t.Fatalf("unexpected journal operation: %v", entry.Step.Op()) 987 } 988 } 989 } 990 991 return res 992 }, 993 }} 994 p.Run(t, old) 995 996 // Test that unknown stacks are handled appropriately. 997 program = deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error { 998 _, _, _, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{ 999 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 1000 "name": "rehto", 1001 }), 1002 }) 1003 assert.Error(t, err) 1004 return err 1005 }) 1006 p.Options = UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)} 1007 p.Steps = []TestStep{{ 1008 Op: Update, 1009 ExpectFailure: true, 1010 SkipPreview: true, 1011 }} 1012 p.Run(t, nil) 1013 1014 // Test that unknown properties cause errors. 1015 program = deploytest.NewLanguageRuntime(func(info plugin.RunInfo, mon *deploytest.ResourceMonitor) error { 1016 _, _, _, err := mon.RegisterResource("pulumi:pulumi:StackReference", "other", true, deploytest.ResourceOptions{ 1017 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 1018 "name": "other", 1019 "foo": "bar", 1020 }), 1021 }) 1022 assert.Error(t, err) 1023 return err 1024 }) 1025 p.Options = UpdateOptions{Host: deploytest.NewPluginHost(nil, nil, program, loaders...)} 1026 p.Run(t, nil) 1027 } 1028 1029 type channelWriter struct { 1030 channel chan []byte 1031 } 1032 1033 func (cw *channelWriter) Write(d []byte) (int, error) { 1034 cw.channel <- d 1035 return len(d), nil 1036 } 1037 1038 // Tests that a failed plugin load correctly shuts down the host. 1039 func TestLoadFailureShutdown(t *testing.T) { 1040 t.Parallel() 1041 1042 // Note that the setup here is a bit baroque, and is intended to replicate the CLI architecture that lead to 1043 // issue #2170. That issue--a panic on a closed channel--was caused by the intersection of several design choices: 1044 // 1045 // - The provider registry loads and configures the set of providers necessary for the resources currently in the 1046 // checkpoint it is processing at plan creation time. Registry creation fails promptly if a provider plugin 1047 // fails to load (e.g. because is binary is missing). 1048 // - Provider configuration in the CLI's host happens asynchronously. This is meant to allow the engine to remain 1049 // responsive while plugins configure. 1050 // - Providers may call back into the CLI's host for logging. Callbacks are processed as long as the CLI's plugin 1051 // context is open. 1052 // - Log events from the CLI's host are delivered to the CLI's diagnostic streams via channels. The CLI closes 1053 // these channels once the engine operation it initiated completes. 1054 // 1055 // These choices gave rise to the following situation: 1056 // 1. The provider registry loads a provider for package A and kicks off its configuration. 1057 // 2. The provider registry attempts to load a provider for package B. The load fails, and the provider registry 1058 // creation call fails promptly. 1059 // 3. The engine operation requested by the CLI fails promptly because provider registry creation failed. 1060 // 4. The CLI shuts down its diagnostic channels. 1061 // 5. The provider for package A calls back in to the host to log a message. The host then attempts to deliver 1062 // the message to the CLI's diagnostic channels, causing a panic. 1063 // 1064 // The fix was to properly close the plugin host during step (3) s.t. the host was no longer accepting callbacks 1065 // and would not attempt to send messages to the CLI's diagnostic channels. 1066 // 1067 // As such, this test attempts to replicate the CLI architecture by using one provider that configures 1068 // asynchronously and attempts to call back into the engine and a second provider that fails to load. 1069 1070 release, done := make(chan bool), make(chan bool) 1071 sinkWriter := &channelWriter{channel: make(chan []byte)} 1072 1073 loaders := []*deploytest.ProviderLoader{ 1074 deploytest.NewProviderLoaderWithHost("pkgA", semver.MustParse("1.0.0"), 1075 func(host plugin.Host) (plugin.Provider, error) { 1076 return &deploytest.Provider{ 1077 ConfigureF: func(news resource.PropertyMap) error { 1078 go func() { 1079 <-release 1080 host.Log(diag.Info, "", "configuring pkgA provider...", 0) 1081 close(done) 1082 }() 1083 return nil 1084 }, 1085 }, nil 1086 }), 1087 deploytest.NewProviderLoader("pkgB", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1088 return nil, errors.New("pkgB load failure") 1089 }), 1090 } 1091 1092 p := &TestPlan{} 1093 provAURN := p.NewProviderURN("pkgA", "default", "") 1094 provBURN := p.NewProviderURN("pkgB", "default", "") 1095 1096 old := &deploy.Snapshot{ 1097 Resources: []*resource.State{ 1098 { 1099 Type: provAURN.Type(), 1100 URN: provAURN, 1101 Custom: true, 1102 ID: "0", 1103 Inputs: resource.PropertyMap{}, 1104 Outputs: resource.PropertyMap{}, 1105 }, 1106 { 1107 Type: provBURN.Type(), 1108 URN: provBURN, 1109 Custom: true, 1110 ID: "1", 1111 Inputs: resource.PropertyMap{}, 1112 Outputs: resource.PropertyMap{}, 1113 }, 1114 }, 1115 } 1116 1117 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1118 return nil 1119 }) 1120 1121 op := TestOp(Update) 1122 sink := diag.DefaultSink(sinkWriter, sinkWriter, diag.FormatOptions{Color: colors.Raw}) 1123 options := UpdateOptions{Host: deploytest.NewPluginHost(sink, sink, program, loaders...)} 1124 project, target := p.GetProject(), p.GetTarget(t, old) 1125 1126 _, res := op.Run(project, target, options, true, nil, nil) 1127 assertIsErrorOrBailResult(t, res) 1128 1129 close(sinkWriter.channel) 1130 close(release) 1131 <-done 1132 } 1133 1134 func TestSingleResourceIgnoreChanges(t *testing.T) { 1135 t.Parallel() 1136 1137 var expectedIgnoreChanges []string 1138 1139 loaders := []*deploytest.ProviderLoader{ 1140 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1141 return &deploytest.Provider{ 1142 DiffF: func(urn resource.URN, id resource.ID, 1143 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 1144 1145 assert.Equal(t, expectedIgnoreChanges, ignoreChanges) 1146 return plugin.DiffResult{}, nil 1147 }, 1148 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 1149 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 1150 1151 assert.Equal(t, expectedIgnoreChanges, ignoreChanges) 1152 return resource.PropertyMap{}, resource.StatusOK, nil 1153 }, 1154 }, nil 1155 }), 1156 } 1157 1158 updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, ignoreChanges []string, 1159 allowedOps []display.StepOp) *deploy.Snapshot { 1160 expectedIgnoreChanges = ignoreChanges 1161 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1162 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1163 Inputs: props, 1164 IgnoreChanges: ignoreChanges, 1165 }) 1166 assert.NoError(t, err) 1167 return nil 1168 }) 1169 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1170 p := &TestPlan{ 1171 Options: UpdateOptions{Host: host}, 1172 Steps: []TestStep{ 1173 { 1174 Op: Update, 1175 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 1176 events []Event, res result.Result) result.Result { 1177 for _, event := range events { 1178 if event.Type == ResourcePreEvent { 1179 payload := event.Payload().(ResourcePreEventPayload) 1180 assert.Subset(t, allowedOps, []display.StepOp{payload.Metadata.Op}) 1181 } 1182 } 1183 return res 1184 }, 1185 }, 1186 }, 1187 } 1188 return p.Run(t, snap) 1189 } 1190 1191 snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{ 1192 "a": 1, 1193 "b": map[string]interface{}{ 1194 "c": "foo", 1195 }, 1196 }), []string{"a", "b.c"}, []display.StepOp{deploy.OpCreate}) 1197 1198 // Ensure that a change to an ignored property results in an OpSame 1199 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1200 "a": 2, 1201 "b": map[string]interface{}{ 1202 "c": "bar", 1203 }, 1204 }), []string{"a", "b.c"}, []display.StepOp{deploy.OpSame}) 1205 1206 // Ensure that a change to an un-ignored property results in an OpUpdate 1207 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1208 "a": 3, 1209 "b": map[string]interface{}{ 1210 "c": "qux", 1211 }, 1212 }), nil, []display.StepOp{deploy.OpUpdate}) 1213 1214 // Ensure that a removing an ignored property results in an OpSame 1215 snap = updateProgramWithProps(snap, resource.PropertyMap{}, []string{"a", "b"}, []display.StepOp{deploy.OpSame}) 1216 1217 // Ensure that a removing an un-ignored property results in an OpUpdate 1218 snap = updateProgramWithProps(snap, resource.PropertyMap{}, nil, []display.StepOp{deploy.OpUpdate}) 1219 1220 // Ensure that adding an ignored property results in an OpSame 1221 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1222 "a": 4, 1223 "b": map[string]interface{}{ 1224 "c": "zed", 1225 }, 1226 }), []string{"a", "b"}, []display.StepOp{deploy.OpSame}) 1227 1228 // Ensure that adding an un-ignored property results in an OpUpdate 1229 _ = updateProgramWithProps(snap, resource.PropertyMap{ 1230 "c": resource.NewNumberProperty(4), 1231 }, []string{"a", "b"}, []display.StepOp{deploy.OpUpdate}) 1232 } 1233 1234 func TestIgnoreChangesInvalidPaths(t *testing.T) { 1235 t.Parallel() 1236 1237 loaders := []*deploytest.ProviderLoader{ 1238 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1239 return &deploytest.Provider{}, nil 1240 }), 1241 } 1242 1243 program := func(monitor *deploytest.ResourceMonitor) error { 1244 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1245 Inputs: resource.PropertyMap{ 1246 "foo": resource.NewObjectProperty(resource.PropertyMap{ 1247 "bar": resource.NewStringProperty("baz"), 1248 }), 1249 "qux": resource.NewArrayProperty([]resource.PropertyValue{ 1250 resource.NewStringProperty("zed"), 1251 }), 1252 }, 1253 }) 1254 assert.NoError(t, err) 1255 return nil 1256 } 1257 1258 runtime := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1259 return program(monitor) 1260 }) 1261 host := deploytest.NewPluginHost(nil, nil, runtime, loaders...) 1262 1263 p := &TestPlan{ 1264 Options: UpdateOptions{Host: host}, 1265 } 1266 1267 project := p.GetProject() 1268 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 1269 assert.Nil(t, res) 1270 1271 program = func(monitor *deploytest.ResourceMonitor) error { 1272 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1273 Inputs: resource.PropertyMap{}, 1274 IgnoreChanges: []string{"foo.bar"}, 1275 }) 1276 assert.Error(t, err) 1277 return nil 1278 } 1279 1280 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1281 assert.NotNil(t, res) 1282 1283 program = func(monitor *deploytest.ResourceMonitor) error { 1284 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1285 Inputs: resource.PropertyMap{}, 1286 IgnoreChanges: []string{"qux[0]"}, 1287 }) 1288 assert.Error(t, err) 1289 return nil 1290 } 1291 1292 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1293 assert.NotNil(t, res) 1294 } 1295 1296 type DiffFunc = func(urn resource.URN, id resource.ID, 1297 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) 1298 1299 func replaceOnChangesTest(t *testing.T, name string, diffFunc DiffFunc) { 1300 t.Run(name, func(t *testing.T) { 1301 t.Parallel() 1302 1303 loaders := []*deploytest.ProviderLoader{ 1304 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1305 return &deploytest.Provider{ 1306 DiffF: diffFunc, 1307 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 1308 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 1309 return news, resource.StatusOK, nil 1310 }, 1311 CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64, 1312 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 1313 return resource.ID("id123"), inputs, resource.StatusOK, nil 1314 }, 1315 }, nil 1316 }), 1317 } 1318 1319 updateProgramWithProps := func(snap *deploy.Snapshot, props resource.PropertyMap, replaceOnChanges []string, 1320 allowedOps []display.StepOp) *deploy.Snapshot { 1321 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1322 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1323 Inputs: props, 1324 ReplaceOnChanges: replaceOnChanges, 1325 }) 1326 assert.NoError(t, err) 1327 return nil 1328 }) 1329 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1330 p := &TestPlan{ 1331 Options: UpdateOptions{Host: host}, 1332 Steps: []TestStep{ 1333 { 1334 Op: Update, 1335 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 1336 events []Event, res result.Result) result.Result { 1337 for _, event := range events { 1338 if event.Type == ResourcePreEvent { 1339 payload := event.Payload().(ResourcePreEventPayload) 1340 assert.Subset(t, allowedOps, []display.StepOp{payload.Metadata.Op}) 1341 } 1342 } 1343 return res 1344 }, 1345 }, 1346 }, 1347 } 1348 return p.Run(t, snap) 1349 } 1350 1351 snap := updateProgramWithProps(nil, resource.NewPropertyMapFromMap(map[string]interface{}{ 1352 "a": 1, 1353 "b": map[string]interface{}{ 1354 "c": "foo", 1355 }, 1356 }), []string{"a", "b.c"}, []display.StepOp{deploy.OpCreate}) 1357 1358 // Ensure that a change to a replaceOnChange property results in an OpReplace 1359 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1360 "a": 2, 1361 "b": map[string]interface{}{ 1362 "c": "foo", 1363 }, 1364 }), []string{"a"}, []display.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}) 1365 1366 // Ensure that a change to a nested replaceOnChange property results in an OpReplace 1367 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1368 "a": 2, 1369 "b": map[string]interface{}{ 1370 "c": "bar", 1371 }, 1372 }), []string{"b.c"}, []display.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}) 1373 1374 // Ensure that a change to any property of a "*" replaceOnChange results in an OpReplace 1375 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1376 "a": 3, 1377 "b": map[string]interface{}{ 1378 "c": "baz", 1379 }, 1380 }), []string{"*"}, []display.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}) 1381 1382 // Ensure that a change to an non-replaceOnChange property results in an OpUpdate 1383 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1384 "a": 4, 1385 "b": map[string]interface{}{ 1386 "c": "qux", 1387 }, 1388 }), nil, []display.StepOp{deploy.OpUpdate}) 1389 1390 // We ensure that we are listing to the engine diff function only when the provider function 1391 // is nil. We do this by adding some weirdness to the provider diff function. 1392 allowed := []display.StepOp{deploy.OpCreateReplacement, deploy.OpReplace, deploy.OpDeleteReplaced} 1393 if diffFunc != nil { 1394 allowed = []display.StepOp{deploy.OpSame} 1395 } 1396 snap = updateProgramWithProps(snap, resource.NewPropertyMapFromMap(map[string]interface{}{ 1397 "a": 42, // 42 is a special value in the "provider" diff function. 1398 "b": map[string]interface{}{ 1399 "c": "qux", 1400 }, 1401 }), []string{"a"}, allowed) 1402 1403 _ = snap 1404 }) 1405 } 1406 1407 func TestReplaceOnChanges(t *testing.T) { 1408 t.Parallel() 1409 1410 // We simulate a provider that has it's own diff function. 1411 replaceOnChangesTest(t, "provider diff", 1412 func(urn resource.URN, id resource.ID, 1413 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 1414 1415 // To establish a observable difference between the provider and engine diff function, 1416 // we treat 42 as an OpSame. We use this to check that the right diff function is being 1417 // used. 1418 for k, v := range news { 1419 if v == resource.NewNumberProperty(42) { 1420 news[k] = olds[k] 1421 } 1422 } 1423 diff := olds.Diff(news) 1424 if diff == nil { 1425 return plugin.DiffResult{Changes: plugin.DiffNone}, nil 1426 } 1427 detailedDiff := plugin.NewDetailedDiffFromObjectDiff(diff) 1428 changedKeys := diff.ChangedKeys() 1429 1430 return plugin.DiffResult{ 1431 Changes: plugin.DiffSome, 1432 ChangedKeys: changedKeys, 1433 DetailedDiff: detailedDiff, 1434 }, nil 1435 }) 1436 1437 // We simulate a provider that does not have it's own diff function. This tests the engines diff 1438 // function instead. 1439 replaceOnChangesTest(t, "engine diff", nil) 1440 } 1441 1442 // Resource is an abstract representation of a resource graph 1443 type Resource struct { 1444 t tokens.Type 1445 name string 1446 children []Resource 1447 props resource.PropertyMap 1448 aliasURNs []resource.URN 1449 aliases []resource.Alias 1450 dependencies []resource.URN 1451 parent resource.URN 1452 deleteBeforeReplace bool 1453 } 1454 1455 func registerResources(t *testing.T, monitor *deploytest.ResourceMonitor, resources []Resource) error { 1456 for _, r := range resources { 1457 _, _, _, err := monitor.RegisterResource(r.t, r.name, true, deploytest.ResourceOptions{ 1458 Parent: r.parent, 1459 Dependencies: r.dependencies, 1460 Inputs: r.props, 1461 DeleteBeforeReplace: &r.deleteBeforeReplace, 1462 AliasURNs: r.aliasURNs, 1463 Aliases: r.aliases, 1464 }) 1465 if err != nil { 1466 return err 1467 } 1468 err = registerResources(t, monitor, r.children) 1469 if err != nil { 1470 return err 1471 } 1472 } 1473 return nil 1474 } 1475 1476 func TestAliases(t *testing.T) { 1477 t.Parallel() 1478 1479 loaders := []*deploytest.ProviderLoader{ 1480 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1481 return &deploytest.Provider{ 1482 // The `forcesReplacement` key forces replacement and all other keys can update in place 1483 DiffF: func(res resource.URN, id resource.ID, olds, news resource.PropertyMap, 1484 ignoreChanges []string) (plugin.DiffResult, error) { 1485 1486 replaceKeys := []resource.PropertyKey{} 1487 old, hasOld := olds["forcesReplacement"] 1488 new, hasNew := news["forcesReplacement"] 1489 if hasOld && !hasNew || hasNew && !hasOld || hasOld && hasNew && old.Diff(new) != nil { 1490 replaceKeys = append(replaceKeys, "forcesReplacement") 1491 } 1492 return plugin.DiffResult{ReplaceKeys: replaceKeys}, nil 1493 }, 1494 }, nil 1495 }), 1496 } 1497 1498 updateProgramWithResource := func( 1499 snap *deploy.Snapshot, resources []Resource, allowedOps []display.StepOp, expectFailure bool) *deploy.Snapshot { 1500 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1501 err := registerResources(t, monitor, resources) 1502 return err 1503 }) 1504 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1505 p := &TestPlan{ 1506 Options: UpdateOptions{Host: host}, 1507 Steps: []TestStep{ 1508 { 1509 Op: Update, 1510 ExpectFailure: expectFailure, 1511 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 1512 events []Event, res result.Result) result.Result { 1513 for _, event := range events { 1514 if event.Type == ResourcePreEvent { 1515 payload := event.Payload().(ResourcePreEventPayload) 1516 assert.Subset(t, allowedOps, []display.StepOp{payload.Metadata.Op}) 1517 } 1518 } 1519 1520 for _, entry := range entries { 1521 if entry.Step.Type() == "pulumi:providers:pkgA" { 1522 continue 1523 } 1524 switch entry.Kind { 1525 case JournalEntrySuccess: 1526 assert.Subset(t, allowedOps, []display.StepOp{entry.Step.Op()}) 1527 case JournalEntryFailure: 1528 assert.Fail(t, "unexpected failure in journal") 1529 case JournalEntryBegin: 1530 case JournalEntryOutputs: 1531 } 1532 } 1533 1534 return res 1535 }, 1536 }, 1537 }, 1538 } 1539 return p.Run(t, snap) 1540 } 1541 1542 snap := updateProgramWithResource(nil, []Resource{{ 1543 t: "pkgA:index:t1", 1544 name: "n1", 1545 }}, []display.StepOp{deploy.OpCreate}, false) 1546 1547 // Ensure that rename produces Same 1548 snap = updateProgramWithResource(snap, []Resource{{ 1549 t: "pkgA:index:t1", 1550 name: "n2", 1551 aliases: []resource.Alias{{URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}}, 1552 }}, []display.StepOp{deploy.OpSame}, false) 1553 1554 // Ensure that rename produces Same with multiple aliases 1555 snap = updateProgramWithResource(snap, []Resource{{ 1556 t: "pkgA:index:t1", 1557 name: "n3", 1558 aliases: []resource.Alias{ 1559 {Name: "n2", Type: "pkgA:index:t1", Stack: "test", Project: "test"}, 1560 {Name: "n1", Type: "pkgA:index:t1", Stack: "test", Project: "test"}, 1561 }, 1562 }}, []display.StepOp{deploy.OpSame}, false) 1563 1564 // Ensure that rename produces Same with multiple aliases (reversed) 1565 snap = updateProgramWithResource(snap, []Resource{{ 1566 t: "pkgA:index:t1", 1567 name: "n3", 1568 aliases: []resource.Alias{ 1569 {URN: "urn:pulumi:test::test::pkgA:index:t1::n2"}, 1570 {Name: "n1", Type: "pkgA:index:t1", Stack: "test", Project: "test"}, 1571 }, 1572 }}, []display.StepOp{deploy.OpSame}, false) 1573 1574 // Ensure that aliasing back to original name is okay 1575 snap = updateProgramWithResource(snap, []Resource{{ 1576 t: "pkgA:index:t1", 1577 name: "n1", 1578 aliases: []resource.Alias{ 1579 {URN: "urn:pulumi:test::test::pkgA:index:t1::n3"}, 1580 {Name: "n2", Type: "pkgA:index:t1", Stack: "test", Project: "test"}, 1581 }, 1582 aliasURNs: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n3"}, 1583 }}, []display.StepOp{deploy.OpSame}, false) 1584 1585 // Ensure that removing aliases is okay (once old names are gone from all snapshots) 1586 snap = updateProgramWithResource(snap, []Resource{{ 1587 t: "pkgA:index:t1", 1588 name: "n1", 1589 }}, []display.StepOp{deploy.OpSame}, false) 1590 1591 // Ensure that changing the type works 1592 snap = updateProgramWithResource(snap, []Resource{{ 1593 t: "pkgA:index:t2", 1594 name: "n1", 1595 aliases: []resource.Alias{ 1596 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1597 }, 1598 }}, []display.StepOp{deploy.OpSame}, false) 1599 1600 // Ensure that changing the type again works 1601 snap = updateProgramWithResource(snap, []Resource{{ 1602 t: "pkgA:othermod:t3", 1603 name: "n1", 1604 aliases: []resource.Alias{ 1605 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1606 {URN: "urn:pulumi:test::test::pkgA:index:t2::n1"}, 1607 }, 1608 }}, []display.StepOp{deploy.OpSame}, false) 1609 1610 // Ensure that order of aliases doesn't matter 1611 snap = updateProgramWithResource(snap, []Resource{{ 1612 t: "pkgA:othermod:t3", 1613 name: "n1", 1614 aliases: []resource.Alias{ 1615 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1616 {URN: "urn:pulumi:test::test::pkgA:othermod:t3::n1"}, 1617 {URN: "urn:pulumi:test::test::pkgA:index:t2::n1"}, 1618 }, 1619 }}, []display.StepOp{deploy.OpSame}, false) 1620 1621 // Ensure that removing aliases is okay (once old names are gone from all snapshots) 1622 snap = updateProgramWithResource(snap, []Resource{{ 1623 t: "pkgA:othermod:t3", 1624 name: "n1", 1625 }}, []display.StepOp{deploy.OpSame}, false) 1626 1627 // Ensure that changing everything (including props) leads to update not delete and re-create 1628 snap = updateProgramWithResource(snap, []Resource{{ 1629 t: "pkgA:index:t4", 1630 name: "n2", 1631 props: resource.PropertyMap{ 1632 resource.PropertyKey("x"): resource.NewNumberProperty(42), 1633 }, 1634 aliases: []resource.Alias{ 1635 {URN: "urn:pulumi:test::test::pkgA:othermod:t3::n1"}, 1636 }, 1637 }}, []display.StepOp{deploy.OpUpdate}, false) 1638 1639 // Ensure that changing everything again (including props) leads to update not delete and re-create 1640 snap = updateProgramWithResource(snap, []Resource{{ 1641 t: "pkgA:index:t5", 1642 name: "n3", 1643 props: resource.PropertyMap{ 1644 resource.PropertyKey("x"): resource.NewNumberProperty(1000), 1645 }, 1646 aliases: []resource.Alias{ 1647 {URN: "urn:pulumi:test::test::pkgA:index:t4::n2"}, 1648 }, 1649 }}, []display.StepOp{deploy.OpUpdate}, false) 1650 1651 // Ensure that changing a forceNew property while also changing type and name leads to replacement not delete+create 1652 snap = updateProgramWithResource(snap, []Resource{{ 1653 t: "pkgA:index:t6", 1654 name: "n4", 1655 props: resource.PropertyMap{ 1656 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1000), 1657 }, 1658 aliases: []resource.Alias{ 1659 {URN: "urn:pulumi:test::test::pkgA:index:t5::n3"}, 1660 }, 1661 }}, []display.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 1662 1663 // Ensure that changing a forceNew property and deleteBeforeReplace while also changing type and name leads to 1664 // replacement not delete+create 1665 _ = updateProgramWithResource(snap, []Resource{{ 1666 t: "pkgA:index:t7", 1667 name: "n5", 1668 props: resource.PropertyMap{ 1669 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(999), 1670 }, 1671 deleteBeforeReplace: true, 1672 aliases: []resource.Alias{ 1673 {URN: "urn:pulumi:test::test::pkgA:index:t6::n4"}, 1674 }, 1675 }}, []display.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 1676 1677 // Start again - this time with two resources with depends on relationship 1678 snap = updateProgramWithResource(nil, []Resource{{ 1679 t: "pkgA:index:t1", 1680 name: "n1", 1681 props: resource.PropertyMap{ 1682 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1), 1683 }, 1684 deleteBeforeReplace: true, 1685 }, { 1686 t: "pkgA:index:t2", 1687 name: "n2", 1688 dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n1"}, 1689 }}, []display.StepOp{deploy.OpCreate}, false) 1690 1691 _ = updateProgramWithResource(snap, []Resource{{ 1692 t: "pkgA:index:t1-new", 1693 name: "n1-new", 1694 props: resource.PropertyMap{ 1695 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2), 1696 }, 1697 deleteBeforeReplace: true, 1698 aliases: []resource.Alias{ 1699 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1700 }, 1701 }, { 1702 t: "pkgA:index:t2-new", 1703 name: "n2-new", 1704 dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1-new::n1-new"}, 1705 aliases: []resource.Alias{ 1706 {URN: "urn:pulumi:test::test::pkgA:index:t2::n2"}, 1707 }, 1708 }}, []display.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 1709 1710 // Start again - this time with two resources with parent relationship 1711 snap = updateProgramWithResource(nil, []Resource{{ 1712 t: "pkgA:index:t1", 1713 name: "n1", 1714 props: resource.PropertyMap{ 1715 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1), 1716 }, 1717 deleteBeforeReplace: true, 1718 }, { 1719 t: "pkgA:index:t2", 1720 name: "n2", 1721 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1::n1"), 1722 }}, []display.StepOp{deploy.OpCreate}, false) 1723 1724 _ = updateProgramWithResource(snap, []Resource{{ 1725 t: "pkgA:index:t1-new", 1726 name: "n1-new", 1727 props: resource.PropertyMap{ 1728 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2), 1729 }, 1730 deleteBeforeReplace: true, 1731 aliases: []resource.Alias{ 1732 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1733 }, 1734 }, { 1735 t: "pkgA:index:t2-new", 1736 name: "n2-new", 1737 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new::n1-new"), 1738 aliases: []resource.Alias{ 1739 {URN: "urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n2"}, 1740 }, 1741 }}, []display.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 1742 1743 // ensure failure when different resources use duplicate aliases 1744 _ = updateProgramWithResource(snap, []Resource{{ 1745 t: "pkgA:index:t1", 1746 name: "n2", 1747 aliases: []resource.Alias{ 1748 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1749 }, 1750 }, { 1751 t: "pkgA:index:t2", 1752 name: "n3", 1753 aliases: []resource.Alias{ 1754 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1755 }, 1756 }}, []display.StepOp{deploy.OpCreate}, true) 1757 1758 // ensure different resources can use different aliases 1759 _ = updateProgramWithResource(nil, []Resource{{ 1760 t: "pkgA:index:t1", 1761 name: "n1", 1762 aliases: []resource.Alias{ 1763 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1764 }, 1765 }, { 1766 t: "pkgA:index:t2", 1767 name: "n2", 1768 aliases: []resource.Alias{ 1769 {URN: "urn:pulumi:test::test::pkgA:index:t1::n2"}, 1770 }, 1771 }}, []display.StepOp{deploy.OpCreate}, false) 1772 1773 // ensure that aliases of parents of parents resolves correctly 1774 // first create a chain of resources such that we have n1 -> n1-sub -> n1-sub-sub 1775 snap = updateProgramWithResource(nil, []Resource{{ 1776 t: "pkgA:index:t1", 1777 name: "n1", 1778 }, { 1779 t: "pkgA:index:t2", 1780 name: "n1-sub", 1781 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1::n1"), 1782 }, { 1783 t: "pkgA:index:t3", 1784 name: "n1-sub-sub", 1785 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n1-sub"), 1786 }}, []display.StepOp{deploy.OpCreate}, false) 1787 1788 // Now change n1's name and type 1789 _ = updateProgramWithResource(snap, []Resource{{ 1790 t: "pkgA:index:t1-new", 1791 name: "n1-new", 1792 aliases: []resource.Alias{ 1793 {URN: "urn:pulumi:test::test::pkgA:index:t1::n1"}, 1794 }, 1795 }, { 1796 t: "pkgA:index:t2", 1797 name: "n1-new-sub", 1798 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new::n1-new"), 1799 aliases: []resource.Alias{ 1800 {URN: "urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n1-sub"}, 1801 }, 1802 }, { 1803 t: "pkgA:index:t3", 1804 name: "n1-new-sub-sub", 1805 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new$pkgA:index:t2::n1-new-sub"), 1806 aliases: []resource.Alias{ 1807 {URN: "urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2$pkgA:index:t3::n1-sub-sub"}, 1808 }, 1809 }}, []display.StepOp{deploy.OpSame}, false) 1810 1811 // Test catastrophic multiplication out of aliases doesn't crash out of memory 1812 // first create a chain of resources such that we have n1 -> n1-sub -> n1-sub-sub 1813 snap = updateProgramWithResource(nil, []Resource{{ 1814 t: "pkgA:index:t1-v0", 1815 name: "n1", 1816 }, { 1817 t: "pkgA:index:t2-v0", 1818 name: "n1-sub", 1819 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v0::n1"), 1820 }, { 1821 t: "pkgA:index:t3", 1822 name: "n1-sub-sub", 1823 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v0$pkgA:index:t2-v0::n1-sub"), 1824 }}, []display.StepOp{deploy.OpCreate}, false) 1825 1826 // Now change n1's name and type and n2's type, but also add a load of aliases and pre-multiply them out 1827 // before sending to the engine 1828 n1Aliases := make([]resource.Alias, 0) 1829 n2Aliases := make([]resource.Alias, 0) 1830 n3Aliases := make([]resource.Alias, 0) 1831 for i := 0; i < 100; i++ { 1832 n1Aliases = append(n1Aliases, resource.Alias{URN: resource.URN( 1833 fmt.Sprintf("urn:pulumi:test::test::pkgA:index:t1-v%d::n1", i), 1834 )}) 1835 1836 for j := 0; j < 10; j++ { 1837 n2Aliases = append(n2Aliases, resource.Alias{ 1838 URN: resource.URN(fmt.Sprintf("urn:pulumi:test::test::pkgA:index:t1-v%d$pkgA:index:t2-v%d::n1-sub", i, j))}) 1839 n3Aliases = append(n3Aliases, resource.Alias{ 1840 Name: "n1-sub-sub", 1841 Type: fmt.Sprintf("pkgA:index:t1-v%d$pkgA:index:t2-v%d$pkgA:index:t3", i, j), 1842 Stack: "test", 1843 Project: "test", 1844 }) 1845 } 1846 } 1847 1848 snap = updateProgramWithResource(snap, []Resource{{ 1849 t: "pkgA:index:t1-v100", 1850 name: "n1-new", 1851 aliases: n1Aliases, 1852 }, { 1853 t: "pkgA:index:t2-v10", 1854 name: "n1-new-sub", 1855 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v100::n1-new"), 1856 aliases: n2Aliases, 1857 }, { 1858 t: "pkgA:index:t3", 1859 name: "n1-new-sub-sub", 1860 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v100$pkgA:index:t2-v10::n1-new-sub"), 1861 aliases: n3Aliases, 1862 }}, []display.StepOp{deploy.OpSame}, false) 1863 1864 var err error 1865 _, err = snap.NormalizeURNReferences() 1866 assert.Nil(t, err) 1867 } 1868 1869 func TestAliasURNs(t *testing.T) { 1870 t.Parallel() 1871 1872 loaders := []*deploytest.ProviderLoader{ 1873 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1874 return &deploytest.Provider{ 1875 // The `forcesReplacement` key forces replacement and all other keys can update in place 1876 DiffF: func(res resource.URN, id resource.ID, olds, news resource.PropertyMap, 1877 ignoreChanges []string) (plugin.DiffResult, error) { 1878 1879 replaceKeys := []resource.PropertyKey{} 1880 old, hasOld := olds["forcesReplacement"] 1881 new, hasNew := news["forcesReplacement"] 1882 if hasOld && !hasNew || hasNew && !hasOld || hasOld && hasNew && old.Diff(new) != nil { 1883 replaceKeys = append(replaceKeys, "forcesReplacement") 1884 } 1885 return plugin.DiffResult{ReplaceKeys: replaceKeys}, nil 1886 }, 1887 }, nil 1888 }), 1889 } 1890 1891 updateProgramWithResource := func( 1892 snap *deploy.Snapshot, resources []Resource, allowedOps []display.StepOp, expectFailure bool) *deploy.Snapshot { 1893 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1894 err := registerResources(t, monitor, resources) 1895 return err 1896 }) 1897 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1898 p := &TestPlan{ 1899 Options: UpdateOptions{Host: host}, 1900 Steps: []TestStep{ 1901 { 1902 Op: Update, 1903 ExpectFailure: expectFailure, 1904 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 1905 events []Event, res result.Result) result.Result { 1906 for _, event := range events { 1907 if event.Type == ResourcePreEvent { 1908 payload := event.Payload().(ResourcePreEventPayload) 1909 assert.Subset(t, allowedOps, []display.StepOp{payload.Metadata.Op}) 1910 } 1911 } 1912 1913 for _, entry := range entries { 1914 if entry.Step.Type() == "pulumi:providers:pkgA" { 1915 continue 1916 } 1917 switch entry.Kind { 1918 case JournalEntrySuccess: 1919 assert.Subset(t, allowedOps, []display.StepOp{entry.Step.Op()}) 1920 case JournalEntryFailure: 1921 assert.Fail(t, "unexpected failure in journal") 1922 case JournalEntryBegin: 1923 case JournalEntryOutputs: 1924 } 1925 } 1926 1927 return res 1928 }, 1929 }, 1930 }, 1931 } 1932 return p.Run(t, snap) 1933 } 1934 1935 snap := updateProgramWithResource(nil, []Resource{{ 1936 t: "pkgA:index:t1", 1937 name: "n1", 1938 }}, []display.StepOp{deploy.OpCreate}, false) 1939 1940 // Ensure that rename produces Same 1941 snap = updateProgramWithResource(snap, []Resource{{ 1942 t: "pkgA:index:t1", 1943 name: "n2", 1944 aliasURNs: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n1"}, 1945 }}, []display.StepOp{deploy.OpSame}, false) 1946 1947 // Ensure that rename produces Same with multiple aliases 1948 snap = updateProgramWithResource(snap, []Resource{{ 1949 t: "pkgA:index:t1", 1950 name: "n3", 1951 aliasURNs: []resource.URN{ 1952 "urn:pulumi:test::test::pkgA:index:t1::n1", 1953 "urn:pulumi:test::test::pkgA:index:t1::n2", 1954 }, 1955 }}, []display.StepOp{deploy.OpSame}, false) 1956 1957 // Ensure that rename produces Same with multiple aliases (reversed) 1958 snap = updateProgramWithResource(snap, []Resource{{ 1959 t: "pkgA:index:t1", 1960 name: "n3", 1961 aliasURNs: []resource.URN{ 1962 "urn:pulumi:test::test::pkgA:index:t1::n2", 1963 "urn:pulumi:test::test::pkgA:index:t1::n1", 1964 }, 1965 }}, []display.StepOp{deploy.OpSame}, false) 1966 1967 // Ensure that aliasing back to original name is okay 1968 snap = updateProgramWithResource(snap, []Resource{{ 1969 t: "pkgA:index:t1", 1970 name: "n1", 1971 aliasURNs: []resource.URN{ 1972 "urn:pulumi:test::test::pkgA:index:t1::n3", 1973 "urn:pulumi:test::test::pkgA:index:t1::n2", 1974 "urn:pulumi:test::test::pkgA:index:t1::n1", 1975 }, 1976 }}, []display.StepOp{deploy.OpSame}, false) 1977 1978 // Ensure that removing aliases is okay (once old names are gone from all snapshots) 1979 snap = updateProgramWithResource(snap, []Resource{{ 1980 t: "pkgA:index:t1", 1981 name: "n1", 1982 }}, []display.StepOp{deploy.OpSame}, false) 1983 1984 // Ensure that changing the type works 1985 snap = updateProgramWithResource(snap, []Resource{{ 1986 t: "pkgA:index:t2", 1987 name: "n1", 1988 aliasURNs: []resource.URN{ 1989 "urn:pulumi:test::test::pkgA:index:t1::n1", 1990 }, 1991 }}, []display.StepOp{deploy.OpSame}, false) 1992 1993 // Ensure that changing the type again works 1994 snap = updateProgramWithResource(snap, []Resource{{ 1995 t: "pkgA:othermod:t3", 1996 name: "n1", 1997 aliasURNs: []resource.URN{ 1998 "urn:pulumi:test::test::pkgA:index:t1::n1", 1999 "urn:pulumi:test::test::pkgA:index:t2::n1", 2000 }, 2001 }}, []display.StepOp{deploy.OpSame}, false) 2002 2003 // Ensure that order of aliases doesn't matter 2004 snap = updateProgramWithResource(snap, []Resource{{ 2005 t: "pkgA:othermod:t3", 2006 name: "n1", 2007 aliasURNs: []resource.URN{ 2008 "urn:pulumi:test::test::pkgA:index:t1::n1", 2009 "urn:pulumi:test::test::pkgA:othermod:t3::n1", 2010 "urn:pulumi:test::test::pkgA:index:t2::n1", 2011 }, 2012 }}, []display.StepOp{deploy.OpSame}, false) 2013 2014 // Ensure that removing aliases is okay (once old names are gone from all snapshots) 2015 snap = updateProgramWithResource(snap, []Resource{{ 2016 t: "pkgA:othermod:t3", 2017 name: "n1", 2018 }}, []display.StepOp{deploy.OpSame}, false) 2019 2020 // Ensure that changing everything (including props) leads to update not delete and re-create 2021 snap = updateProgramWithResource(snap, []Resource{{ 2022 t: "pkgA:index:t4", 2023 name: "n2", 2024 props: resource.PropertyMap{ 2025 resource.PropertyKey("x"): resource.NewNumberProperty(42), 2026 }, 2027 aliasURNs: []resource.URN{ 2028 "urn:pulumi:test::test::pkgA:othermod:t3::n1", 2029 }, 2030 }}, []display.StepOp{deploy.OpUpdate}, false) 2031 2032 // Ensure that changing everything again (including props) leads to update not delete and re-create 2033 snap = updateProgramWithResource(snap, []Resource{{ 2034 t: "pkgA:index:t5", 2035 name: "n3", 2036 props: resource.PropertyMap{ 2037 resource.PropertyKey("x"): resource.NewNumberProperty(1000), 2038 }, 2039 aliasURNs: []resource.URN{ 2040 "urn:pulumi:test::test::pkgA:index:t4::n2", 2041 }, 2042 }}, []display.StepOp{deploy.OpUpdate}, false) 2043 2044 // Ensure that changing a forceNew property while also changing type and name leads to replacement not delete+create 2045 snap = updateProgramWithResource(snap, []Resource{{ 2046 t: "pkgA:index:t6", 2047 name: "n4", 2048 props: resource.PropertyMap{ 2049 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1000), 2050 }, 2051 aliasURNs: []resource.URN{ 2052 "urn:pulumi:test::test::pkgA:index:t5::n3", 2053 }, 2054 }}, []display.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 2055 2056 // Ensure that changing a forceNew property and deleteBeforeReplace while also changing type and name leads to 2057 // replacement not delete+create 2058 _ = updateProgramWithResource(snap, []Resource{{ 2059 t: "pkgA:index:t7", 2060 name: "n5", 2061 props: resource.PropertyMap{ 2062 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(999), 2063 }, 2064 deleteBeforeReplace: true, 2065 aliasURNs: []resource.URN{ 2066 "urn:pulumi:test::test::pkgA:index:t6::n4", 2067 }, 2068 }}, []display.StepOp{deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 2069 2070 // Start again - this time with two resources with depends on relationship 2071 snap = updateProgramWithResource(nil, []Resource{{ 2072 t: "pkgA:index:t1", 2073 name: "n1", 2074 props: resource.PropertyMap{ 2075 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1), 2076 }, 2077 deleteBeforeReplace: true, 2078 }, { 2079 t: "pkgA:index:t2", 2080 name: "n2", 2081 dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1::n1"}, 2082 }}, []display.StepOp{deploy.OpCreate}, false) 2083 2084 _ = updateProgramWithResource(snap, []Resource{{ 2085 t: "pkgA:index:t1-new", 2086 name: "n1-new", 2087 props: resource.PropertyMap{ 2088 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2), 2089 }, 2090 deleteBeforeReplace: true, 2091 aliasURNs: []resource.URN{ 2092 "urn:pulumi:test::test::pkgA:index:t1::n1", 2093 }, 2094 }, { 2095 t: "pkgA:index:t2-new", 2096 name: "n2-new", 2097 dependencies: []resource.URN{"urn:pulumi:test::test::pkgA:index:t1-new::n1-new"}, 2098 aliasURNs: []resource.URN{ 2099 "urn:pulumi:test::test::pkgA:index:t2::n2", 2100 }, 2101 }}, []display.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 2102 2103 // Start again - this time with two resources with parent relationship 2104 snap = updateProgramWithResource(nil, []Resource{{ 2105 t: "pkgA:index:t1", 2106 name: "n1", 2107 props: resource.PropertyMap{ 2108 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(1), 2109 }, 2110 deleteBeforeReplace: true, 2111 }, { 2112 t: "pkgA:index:t2", 2113 name: "n2", 2114 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1::n1"), 2115 }}, []display.StepOp{deploy.OpCreate}, false) 2116 2117 _ = updateProgramWithResource(snap, []Resource{{ 2118 t: "pkgA:index:t1-new", 2119 name: "n1-new", 2120 props: resource.PropertyMap{ 2121 resource.PropertyKey("forcesReplacement"): resource.NewNumberProperty(2), 2122 }, 2123 deleteBeforeReplace: true, 2124 aliasURNs: []resource.URN{ 2125 "urn:pulumi:test::test::pkgA:index:t1::n1", 2126 }, 2127 }, { 2128 t: "pkgA:index:t2-new", 2129 name: "n2-new", 2130 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new::n1-new"), 2131 aliasURNs: []resource.URN{ 2132 "urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n2", 2133 }, 2134 }}, []display.StepOp{deploy.OpSame, deploy.OpReplace, deploy.OpCreateReplacement, deploy.OpDeleteReplaced}, false) 2135 2136 // ensure failure when different resources use duplicate aliases 2137 _ = updateProgramWithResource(snap, []Resource{{ 2138 t: "pkgA:index:t1", 2139 name: "n2", 2140 aliasURNs: []resource.URN{ 2141 "urn:pulumi:test::test::pkgA:index:t1::n1", 2142 }, 2143 }, { 2144 t: "pkgA:index:t2", 2145 name: "n3", 2146 aliasURNs: []resource.URN{ 2147 "urn:pulumi:test::test::pkgA:index:t1::n1", 2148 }, 2149 }}, []display.StepOp{deploy.OpCreate}, true) 2150 2151 // ensure different resources can use different aliases 2152 _ = updateProgramWithResource(nil, []Resource{{ 2153 t: "pkgA:index:t1", 2154 name: "n1", 2155 aliasURNs: []resource.URN{ 2156 "urn:pulumi:test::test::pkgA:index:t1::n1", 2157 }, 2158 }, { 2159 t: "pkgA:index:t2", 2160 name: "n2", 2161 aliasURNs: []resource.URN{ 2162 "urn:pulumi:test::test::pkgA:index:t1::n2", 2163 }, 2164 }}, []display.StepOp{deploy.OpCreate}, false) 2165 2166 // ensure that aliases of parents of parents resolves correctly 2167 // first create a chain of resources such that we have n1 -> n1-sub -> n1-sub-sub 2168 snap = updateProgramWithResource(nil, []Resource{{ 2169 t: "pkgA:index:t1", 2170 name: "n1", 2171 }, { 2172 t: "pkgA:index:t2", 2173 name: "n1-sub", 2174 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1::n1"), 2175 }, { 2176 t: "pkgA:index:t3", 2177 name: "n1-sub-sub", 2178 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n1-sub"), 2179 }}, []display.StepOp{deploy.OpCreate}, false) 2180 2181 // Now change n1's name and type 2182 _ = updateProgramWithResource(snap, []Resource{{ 2183 t: "pkgA:index:t1-new", 2184 name: "n1-new", 2185 aliasURNs: []resource.URN{ 2186 "urn:pulumi:test::test::pkgA:index:t1::n1", 2187 }, 2188 }, { 2189 t: "pkgA:index:t2", 2190 name: "n1-new-sub", 2191 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new::n1-new"), 2192 aliasURNs: []resource.URN{ 2193 "urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2::n1-sub", 2194 }, 2195 }, { 2196 t: "pkgA:index:t3", 2197 name: "n1-new-sub-sub", 2198 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-new$pkgA:index:t2::n1-new-sub"), 2199 aliasURNs: []resource.URN{ 2200 "urn:pulumi:test::test::pkgA:index:t1$pkgA:index:t2$pkgA:index:t3::n1-sub-sub", 2201 }, 2202 }}, []display.StepOp{deploy.OpSame}, false) 2203 2204 // Test catastrophic multiplication out of aliases doesn't crash out of memory 2205 // first create a chain of resources such that we have n1 -> n1-sub -> n1-sub-sub 2206 snap = updateProgramWithResource(nil, []Resource{{ 2207 t: "pkgA:index:t1-v0", 2208 name: "n1", 2209 }, { 2210 t: "pkgA:index:t2-v0", 2211 name: "n1-sub", 2212 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v0::n1"), 2213 }, { 2214 t: "pkgA:index:t3", 2215 name: "n1-sub-sub", 2216 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v0$pkgA:index:t2-v0::n1-sub"), 2217 }}, []display.StepOp{deploy.OpCreate}, false) 2218 2219 // Now change n1's name and type and n2's type, but also add a load of aliases and pre-multiply them out 2220 // before sending to the engine 2221 n1Aliases := make([]resource.URN, 0) 2222 n2Aliases := make([]resource.URN, 0) 2223 n3Aliases := make([]resource.URN, 0) 2224 for i := 0; i < 100; i++ { 2225 n1Aliases = append(n1Aliases, resource.URN( 2226 fmt.Sprintf("urn:pulumi:test::test::pkgA:index:t1-v%d::n1", i))) 2227 2228 for j := 0; j < 10; j++ { 2229 n2Aliases = append(n2Aliases, resource.URN( 2230 fmt.Sprintf("urn:pulumi:test::test::pkgA:index:t1-v%d$pkgA:index:t2-v%d::n1-sub", i, j))) 2231 2232 n3Aliases = append(n3Aliases, resource.URN( 2233 fmt.Sprintf("urn:pulumi:test::test::pkgA:index:t1-v%d$pkgA:index:t2-v%d$pkgA:index:t3::n1-sub-sub", i, j))) 2234 } 2235 } 2236 2237 snap = updateProgramWithResource(snap, []Resource{{ 2238 t: "pkgA:index:t1-v100", 2239 name: "n1-new", 2240 aliasURNs: n1Aliases, 2241 }, { 2242 t: "pkgA:index:t2-v10", 2243 name: "n1-new-sub", 2244 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v100::n1-new"), 2245 aliasURNs: n2Aliases, 2246 }, { 2247 t: "pkgA:index:t3", 2248 name: "n1-new-sub-sub", 2249 parent: resource.URN("urn:pulumi:test::test::pkgA:index:t1-v100$pkgA:index:t2-v10::n1-new-sub"), 2250 aliasURNs: n3Aliases, 2251 }}, []display.StepOp{deploy.OpSame}, false) 2252 2253 var err error 2254 _, err = snap.NormalizeURNReferences() 2255 assert.Nil(t, err) 2256 } 2257 2258 func TestPersistentDiff(t *testing.T) { 2259 t.Parallel() 2260 2261 loaders := []*deploytest.ProviderLoader{ 2262 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2263 return &deploytest.Provider{ 2264 DiffF: func(urn resource.URN, id resource.ID, 2265 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 2266 2267 return plugin.DiffResult{Changes: plugin.DiffSome}, nil 2268 }, 2269 }, nil 2270 }), 2271 } 2272 2273 inputs := resource.PropertyMap{} 2274 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2275 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 2276 Inputs: inputs, 2277 }) 2278 assert.NoError(t, err) 2279 return nil 2280 }) 2281 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2282 2283 p := &TestPlan{ 2284 Options: UpdateOptions{Host: host}, 2285 } 2286 resURN := p.NewURN("pkgA:m:typA", "resA", "") 2287 2288 // Run the initial update. 2289 project := p.GetProject() 2290 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 2291 assert.Nil(t, res) 2292 2293 // First, make no change to the inputs and run a preview. We should see an update to the resource due to 2294 // provider diffing. 2295 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient, 2296 func(_ workspace.Project, _ deploy.Target, _ JournalEntries, 2297 events []Event, res result.Result) result.Result { 2298 2299 found := false 2300 for _, e := range events { 2301 if e.Type == ResourcePreEvent { 2302 p := e.Payload().(ResourcePreEventPayload).Metadata 2303 if p.URN == resURN { 2304 assert.Equal(t, deploy.OpUpdate, p.Op) 2305 found = true 2306 } 2307 } 2308 } 2309 assert.True(t, found) 2310 return res 2311 }) 2312 assert.Nil(t, res) 2313 2314 // Next, enable legacy diff behavior. We should see no changes to the resource. 2315 p.Options.UseLegacyDiff = true 2316 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient, 2317 func(_ workspace.Project, _ deploy.Target, _ JournalEntries, 2318 events []Event, res result.Result) result.Result { 2319 2320 found := false 2321 for _, e := range events { 2322 if e.Type == ResourcePreEvent { 2323 p := e.Payload().(ResourcePreEventPayload).Metadata 2324 if p.URN == resURN { 2325 assert.Equal(t, deploy.OpSame, p.Op) 2326 found = true 2327 } 2328 } 2329 } 2330 assert.True(t, found) 2331 return res 2332 }) 2333 assert.Nil(t, res) 2334 } 2335 2336 func TestDetailedDiffReplace(t *testing.T) { 2337 t.Parallel() 2338 2339 loaders := []*deploytest.ProviderLoader{ 2340 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2341 return &deploytest.Provider{ 2342 DiffF: func(urn resource.URN, id resource.ID, 2343 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 2344 2345 return plugin.DiffResult{ 2346 Changes: plugin.DiffSome, 2347 DetailedDiff: map[string]plugin.PropertyDiff{ 2348 "prop": {Kind: plugin.DiffAddReplace}, 2349 }, 2350 }, nil 2351 }, 2352 }, nil 2353 }), 2354 } 2355 2356 inputs := resource.PropertyMap{} 2357 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2358 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 2359 Inputs: inputs, 2360 }) 2361 assert.NoError(t, err) 2362 return nil 2363 }) 2364 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2365 2366 p := &TestPlan{ 2367 Options: UpdateOptions{Host: host}, 2368 } 2369 resURN := p.NewURN("pkgA:m:typA", "resA", "") 2370 2371 // Run the initial update. 2372 project := p.GetProject() 2373 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 2374 assert.Nil(t, res) 2375 2376 // First, make no change to the inputs and run a preview. We should see an update to the resource due to 2377 // provider diffing. 2378 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, true, p.BackendClient, 2379 func(_ workspace.Project, _ deploy.Target, _ JournalEntries, 2380 events []Event, res result.Result) result.Result { 2381 2382 found := false 2383 for _, e := range events { 2384 if e.Type == ResourcePreEvent { 2385 p := e.Payload().(ResourcePreEventPayload).Metadata 2386 if p.URN == resURN && p.Op == deploy.OpReplace { 2387 found = true 2388 } 2389 } 2390 } 2391 assert.True(t, found) 2392 return res 2393 }) 2394 assert.Nil(t, res) 2395 } 2396 2397 func TestCustomTimeouts(t *testing.T) { 2398 t.Parallel() 2399 2400 loaders := []*deploytest.ProviderLoader{ 2401 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2402 return &deploytest.Provider{}, nil 2403 }), 2404 } 2405 2406 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2407 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 2408 CustomTimeouts: &resource.CustomTimeouts{ 2409 Create: 60, Delete: 60, Update: 240, 2410 }, 2411 }) 2412 assert.NoError(t, err) 2413 return nil 2414 }) 2415 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2416 2417 p := &TestPlan{ 2418 Options: UpdateOptions{Host: host}, 2419 } 2420 2421 p.Steps = []TestStep{{Op: Update}} 2422 snap := p.Run(t, nil) 2423 2424 assert.Len(t, snap.Resources, 2) 2425 assert.Equal(t, string(snap.Resources[0].URN.Name()), "default") 2426 assert.Equal(t, string(snap.Resources[1].URN.Name()), "resA") 2427 assert.NotNil(t, snap.Resources[1].CustomTimeouts) 2428 assert.Equal(t, snap.Resources[1].CustomTimeouts.Create, float64(60)) 2429 assert.Equal(t, snap.Resources[1].CustomTimeouts.Update, float64(240)) 2430 assert.Equal(t, snap.Resources[1].CustomTimeouts.Delete, float64(60)) 2431 } 2432 2433 func TestProviderDiffMissingOldOutputs(t *testing.T) { 2434 t.Parallel() 2435 2436 loaders := []*deploytest.ProviderLoader{ 2437 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2438 return &deploytest.Provider{ 2439 DiffConfigF: func(urn resource.URN, olds, news resource.PropertyMap, 2440 ignoreChanges []string) (plugin.DiffResult, error) { 2441 // Always require replacement if any diff exists. 2442 if !olds.DeepEquals(news) { 2443 keys := []resource.PropertyKey{} 2444 for k := range news { 2445 keys = append(keys, k) 2446 } 2447 return plugin.DiffResult{Changes: plugin.DiffSome, ReplaceKeys: keys}, nil 2448 } 2449 return plugin.DiffResult{Changes: plugin.DiffNone}, nil 2450 }, 2451 }, nil 2452 }), 2453 } 2454 2455 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2456 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 2457 assert.NoError(t, err) 2458 return nil 2459 }) 2460 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2461 2462 p := &TestPlan{ 2463 Options: UpdateOptions{Host: host}, 2464 Config: config.Map{ 2465 config.MustMakeKey("pkgA", "foo"): config.NewValue("bar"), 2466 }, 2467 } 2468 2469 // Build a basic lifecycle. 2470 steps := MakeBasicLifecycleSteps(t, 2) 2471 2472 // Run the lifecycle through its initial update and refresh. 2473 p.Steps = steps[:2] 2474 snap := p.Run(t, nil) 2475 2476 // Delete the old provider outputs (if any) from the checkpoint, then run the no-op update. 2477 providerURN := p.NewProviderURN("pkgA", "default", "") 2478 for _, r := range snap.Resources { 2479 if r.URN == providerURN { 2480 r.Outputs = nil 2481 } 2482 } 2483 2484 p.Steps = steps[2:3] 2485 snap = p.Run(t, snap) 2486 2487 // Change the config, delete the old provider outputs, and run an update. We expect everything to require 2488 // replacement. 2489 p.Config[config.MustMakeKey("pkgA", "foo")] = config.NewValue("baz") 2490 for _, r := range snap.Resources { 2491 if r.URN == providerURN { 2492 r.Outputs = nil 2493 } 2494 } 2495 p.Steps = []TestStep{{ 2496 Op: Update, 2497 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 2498 _ []Event, res result.Result) result.Result { 2499 2500 resURN := p.NewURN("pkgA:m:typA", "resA", "") 2501 2502 // Look for replace steps on the provider and the resource. 2503 replacedProvider, replacedResource := false, false 2504 for _, entry := range entries { 2505 if entry.Kind != JournalEntrySuccess || entry.Step.Op() != deploy.OpDeleteReplaced { 2506 continue 2507 } 2508 2509 switch urn := entry.Step.URN(); urn { 2510 case providerURN: 2511 replacedProvider = true 2512 case resURN: 2513 replacedResource = true 2514 default: 2515 t.Fatalf("unexpected resource %v", urn) 2516 } 2517 } 2518 assert.True(t, replacedProvider) 2519 assert.True(t, replacedResource) 2520 2521 return res 2522 }, 2523 }} 2524 p.Run(t, snap) 2525 } 2526 2527 func TestMissingRead(t *testing.T) { 2528 t.Parallel() 2529 2530 loaders := []*deploytest.ProviderLoader{ 2531 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2532 return &deploytest.Provider{ 2533 ReadF: func(_ resource.URN, _ resource.ID, _, _ resource.PropertyMap) (plugin.ReadResult, resource.Status, error) { 2534 return plugin.ReadResult{}, resource.StatusOK, nil 2535 }, 2536 }, nil 2537 }), 2538 } 2539 2540 // Our program reads a resource and exits. 2541 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2542 _, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "resA-some-id", "", resource.PropertyMap{}, "", "") 2543 assert.Error(t, err) 2544 return nil 2545 }) 2546 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2547 p := &TestPlan{ 2548 Options: UpdateOptions{Host: host}, 2549 Steps: []TestStep{{Op: Update, ExpectFailure: true}}, 2550 } 2551 p.Run(t, nil) 2552 } 2553 2554 func TestProviderPreview(t *testing.T) { 2555 t.Parallel() 2556 2557 sawPreview := false 2558 loaders := []*deploytest.ProviderLoader{ 2559 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2560 return &deploytest.Provider{ 2561 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 2562 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 2563 2564 if preview { 2565 sawPreview = true 2566 } 2567 2568 assert.Equal(t, preview, news.ContainsUnknowns()) 2569 return "created-id", news, resource.StatusOK, nil 2570 }, 2571 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 2572 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 2573 2574 if preview { 2575 sawPreview = true 2576 } 2577 2578 assert.Equal(t, preview, news.ContainsUnknowns()) 2579 return news, resource.StatusOK, nil 2580 }, 2581 }, nil 2582 }), 2583 } 2584 2585 preview := true 2586 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2587 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 2588 if !preview { 2589 computed = "alpha" 2590 } 2591 2592 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 2593 "foo": "bar", 2594 "baz": map[string]interface{}{ 2595 "a": 42, 2596 "b": computed, 2597 }, 2598 "qux": []interface{}{ 2599 computed, 2600 24, 2601 }, 2602 "zed": computed, 2603 }) 2604 2605 _, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 2606 Inputs: ins, 2607 }) 2608 assert.NoError(t, err) 2609 2610 assert.True(t, state.DeepEquals(ins)) 2611 2612 return nil 2613 }) 2614 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2615 2616 p := &TestPlan{ 2617 Options: UpdateOptions{Host: host}, 2618 } 2619 2620 project := p.GetProject() 2621 2622 // Run a preview. The inputs should be propagated to the outputs by the provider during the create. 2623 preview, sawPreview = true, false 2624 _, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil) 2625 assert.Nil(t, res) 2626 assert.True(t, sawPreview) 2627 2628 // Run an update. 2629 preview, sawPreview = false, false 2630 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil) 2631 assert.Nil(t, res) 2632 assert.False(t, sawPreview) 2633 2634 // Run another preview. The inputs should be propagated to the outputs during the update. 2635 preview, sawPreview = true, false 2636 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, preview, p.BackendClient, nil) 2637 assert.Nil(t, res) 2638 assert.True(t, sawPreview) 2639 } 2640 2641 func TestProviderPreviewGrpc(t *testing.T) { 2642 t.Parallel() 2643 2644 sawPreview := false 2645 loaders := []*deploytest.ProviderLoader{ 2646 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2647 return &deploytest.Provider{ 2648 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 2649 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 2650 2651 if preview { 2652 sawPreview = true 2653 } 2654 2655 assert.Equal(t, preview, news.ContainsUnknowns()) 2656 return "created-id", news, resource.StatusOK, nil 2657 }, 2658 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 2659 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 2660 2661 if preview { 2662 sawPreview = true 2663 } 2664 2665 assert.Equal(t, preview, news.ContainsUnknowns()) 2666 return news, resource.StatusOK, nil 2667 }, 2668 }, nil 2669 }, deploytest.WithGrpc), 2670 } 2671 2672 preview := true 2673 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2674 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 2675 if !preview { 2676 computed = "alpha" 2677 } 2678 2679 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 2680 "foo": "bar", 2681 "baz": map[string]interface{}{ 2682 "a": 42, 2683 "b": computed, 2684 }, 2685 "qux": []interface{}{ 2686 computed, 2687 24, 2688 }, 2689 "zed": computed, 2690 }) 2691 2692 _, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 2693 Inputs: ins, 2694 }) 2695 assert.NoError(t, err) 2696 2697 assert.True(t, state.DeepEquals(ins)) 2698 2699 return nil 2700 }) 2701 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2702 2703 p := &TestPlan{ 2704 Options: UpdateOptions{Host: host}, 2705 } 2706 2707 project := p.GetProject() 2708 2709 // Run a preview. The inputs should be propagated to the outputs by the provider during the create. 2710 preview, sawPreview = true, false 2711 _, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil) 2712 assert.Nil(t, res) 2713 assert.True(t, sawPreview) 2714 2715 // Run an update. 2716 preview, sawPreview = false, false 2717 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil) 2718 assert.Nil(t, res) 2719 assert.False(t, sawPreview) 2720 2721 // Run another preview. The inputs should be propagated to the outputs during the update. 2722 preview, sawPreview = true, false 2723 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, preview, p.BackendClient, nil) 2724 assert.Nil(t, res) 2725 assert.True(t, sawPreview) 2726 } 2727 2728 func TestProviderPreviewUnknowns(t *testing.T) { 2729 t.Parallel() 2730 2731 sawPreview := false 2732 loaders := []*deploytest.ProviderLoader{ 2733 // NOTE: it is important that this test uses a gRPC-wraped provider. The code that handles previews for unconfigured 2734 // providers is specific to the gRPC layer. 2735 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2736 return &deploytest.Provider{ 2737 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 2738 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 2739 2740 if preview { 2741 sawPreview = true 2742 } 2743 2744 return "created-id", news, resource.StatusOK, nil 2745 }, 2746 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 2747 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 2748 2749 if preview { 2750 sawPreview = true 2751 } 2752 2753 return news, resource.StatusOK, nil 2754 }, 2755 }, nil 2756 }, deploytest.WithGrpc), 2757 } 2758 2759 preview := true 2760 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2761 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 2762 if !preview { 2763 computed = "alpha" 2764 } 2765 2766 provURN, provID, _, err := monitor.RegisterResource("pulumi:providers:pkgA", "provA", true, 2767 deploytest.ResourceOptions{ 2768 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{"foo": computed}), 2769 }) 2770 require.NoError(t, err) 2771 2772 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 2773 "foo": "bar", 2774 "baz": map[string]interface{}{ 2775 "a": 42, 2776 }, 2777 "qux": []interface{}{ 2778 24, 2779 }, 2780 }) 2781 2782 _, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 2783 Inputs: ins, 2784 Provider: fmt.Sprintf("%v::%v", provURN, provID), 2785 }) 2786 require.NoError(t, err) 2787 2788 if preview { 2789 assert.True(t, state.DeepEquals(resource.PropertyMap{})) 2790 } else { 2791 assert.True(t, state.DeepEquals(ins)) 2792 } 2793 2794 return nil 2795 }) 2796 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2797 2798 p := &TestPlan{ 2799 Options: UpdateOptions{Host: host}, 2800 } 2801 2802 project := p.GetProject() 2803 2804 // Run a preview. The inputs should not be propagated to the outputs by the provider during the create because the 2805 // provider has unknown inputs. 2806 preview, sawPreview = true, false 2807 _, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil) 2808 require.Nil(t, res) 2809 assert.False(t, sawPreview) 2810 2811 // Run an update. 2812 preview, sawPreview = false, false 2813 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, preview, p.BackendClient, nil) 2814 require.Nil(t, res) 2815 assert.False(t, sawPreview) 2816 2817 // Run another preview. The inputs should not be propagated to the outputs during the update because the provider 2818 // has unknown inputs. 2819 preview, sawPreview = true, false 2820 _, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, preview, p.BackendClient, nil) 2821 require.Nil(t, res) 2822 assert.False(t, sawPreview) 2823 } 2824 2825 func TestSingleComponentDefaultProviderLifecycle(t *testing.T) { 2826 t.Parallel() 2827 2828 loaders := []*deploytest.ProviderLoader{ 2829 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2830 construct := func(monitor *deploytest.ResourceMonitor, 2831 typ, name string, parent resource.URN, inputs resource.PropertyMap, 2832 options plugin.ConstructOptions) (plugin.ConstructResult, error) { 2833 2834 urn, _, _, err := monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{ 2835 Parent: parent, 2836 Aliases: options.Aliases, 2837 Protect: options.Protect, 2838 }) 2839 assert.NoError(t, err) 2840 2841 _, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{ 2842 Parent: urn, 2843 }) 2844 assert.NoError(t, err) 2845 2846 outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")} 2847 err = monitor.RegisterResourceOutputs(urn, outs) 2848 assert.NoError(t, err) 2849 2850 return plugin.ConstructResult{ 2851 URN: urn, 2852 Outputs: outs, 2853 }, nil 2854 } 2855 2856 return &deploytest.Provider{ 2857 ConstructF: construct, 2858 }, nil 2859 }), 2860 } 2861 2862 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 2863 _, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{ 2864 Remote: true, 2865 }) 2866 assert.NoError(t, err) 2867 assert.Equal(t, resource.PropertyMap{ 2868 "foo": resource.NewStringProperty("bar"), 2869 }, state) 2870 return nil 2871 }) 2872 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 2873 2874 p := &TestPlan{ 2875 Options: UpdateOptions{Host: host}, 2876 Steps: MakeBasicLifecycleSteps(t, 3), 2877 } 2878 p.Run(t, nil) 2879 } 2880 2881 type updateContext struct { 2882 *deploytest.ResourceMonitor 2883 2884 resmon chan *deploytest.ResourceMonitor 2885 programErr chan error 2886 snap chan *deploy.Snapshot 2887 updateResult chan result.Result 2888 } 2889 2890 func startUpdate(t *testing.T, host plugin.Host) (*updateContext, error) { 2891 ctx := &updateContext{ 2892 resmon: make(chan *deploytest.ResourceMonitor), 2893 programErr: make(chan error), 2894 snap: make(chan *deploy.Snapshot), 2895 updateResult: make(chan result.Result), 2896 } 2897 2898 stop := make(chan bool) 2899 port, _, err := rpcutil.Serve(0, stop, []func(*grpc.Server) error{ 2900 func(srv *grpc.Server) error { 2901 pulumirpc.RegisterLanguageRuntimeServer(srv, ctx) 2902 return nil 2903 }, 2904 }, nil) 2905 if err != nil { 2906 return nil, err 2907 } 2908 2909 p := &TestPlan{ 2910 Options: UpdateOptions{Host: host}, 2911 Runtime: "client", 2912 RuntimeOptions: map[string]interface{}{ 2913 "address": fmt.Sprintf("127.0.0.1:%d", port), 2914 }, 2915 } 2916 2917 go func() { 2918 snap, res := TestOp(Update).Run(p.GetProject(), p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 2919 ctx.snap <- snap 2920 close(ctx.snap) 2921 ctx.updateResult <- res 2922 close(ctx.updateResult) 2923 stop <- true 2924 }() 2925 2926 ctx.ResourceMonitor = <-ctx.resmon 2927 return ctx, nil 2928 } 2929 2930 func (ctx *updateContext) Finish(err error) (*deploy.Snapshot, result.Result) { 2931 ctx.programErr <- err 2932 close(ctx.programErr) 2933 2934 return <-ctx.snap, <-ctx.updateResult 2935 } 2936 2937 func (ctx *updateContext) GetRequiredPlugins(_ context.Context, 2938 req *pulumirpc.GetRequiredPluginsRequest) (*pulumirpc.GetRequiredPluginsResponse, error) { 2939 return &pulumirpc.GetRequiredPluginsResponse{}, nil 2940 } 2941 2942 func (ctx *updateContext) Run(_ context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) { 2943 // Connect to the resource monitor and create an appropriate client. 2944 conn, err := grpc.Dial( 2945 req.MonitorAddress, 2946 grpc.WithInsecure(), 2947 rpcutil.GrpcChannelOptions(), 2948 ) 2949 if err != nil { 2950 return nil, fmt.Errorf("could not connect to resource monitor: %w", err) 2951 } 2952 defer contract.IgnoreClose(conn) 2953 2954 // Fire up a resource monitor client 2955 ctx.resmon <- deploytest.NewResourceMonitor(pulumirpc.NewResourceMonitorClient(conn)) 2956 close(ctx.resmon) 2957 2958 // Wait for the program to terminate. 2959 if err := <-ctx.programErr; err != nil { 2960 return &pulumirpc.RunResponse{Error: err.Error()}, nil 2961 } 2962 return &pulumirpc.RunResponse{}, nil 2963 } 2964 2965 func (ctx *updateContext) GetPluginInfo(_ context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) { 2966 return &pulumirpc.PluginInfo{ 2967 Version: "1.0.0", 2968 }, nil 2969 } 2970 2971 func (ctx *updateContext) InstallDependencies( 2972 req *pulumirpc.InstallDependenciesRequest, 2973 server pulumirpc.LanguageRuntime_InstallDependenciesServer) error { 2974 return nil 2975 } 2976 2977 func (ctx *updateContext) About(_ context.Context, _ *pbempty.Empty) (*pulumirpc.AboutResponse, error) { 2978 return nil, status.Errorf(codes.Unimplemented, "method About not implemented") 2979 } 2980 2981 func (ctx *updateContext) GetProgramDependencies( 2982 _ context.Context, _ *pulumirpc.GetProgramDependenciesRequest) (*pulumirpc.GetProgramDependenciesResponse, error) { 2983 return nil, status.Errorf(codes.Unimplemented, "method GetProgramDependencies not implemented") 2984 } 2985 2986 func TestLanguageClient(t *testing.T) { 2987 t.Parallel() 2988 2989 loaders := []*deploytest.ProviderLoader{ 2990 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 2991 return &deploytest.Provider{}, nil 2992 }), 2993 } 2994 2995 update, err := startUpdate(t, deploytest.NewPluginHost(nil, nil, nil, loaders...)) 2996 if err != nil { 2997 t.Fatalf("failed to start update: %v", err) 2998 } 2999 3000 // Register resources, etc. 3001 _, _, _, err = update.RegisterResource("pkgA:m:typA", "resA", true) 3002 assert.NoError(t, err) 3003 3004 snap, res := update.Finish(nil) 3005 assert.Nil(t, res) 3006 assert.Len(t, snap.Resources, 2) 3007 } 3008 3009 func TestSingleComponentGetResourceDefaultProviderLifecycle(t *testing.T) { 3010 t.Parallel() 3011 3012 var urnB resource.URN 3013 var idB resource.ID 3014 3015 loaders := []*deploytest.ProviderLoader{ 3016 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3017 construct := func(monitor *deploytest.ResourceMonitor, typ, name string, parent resource.URN, 3018 inputs resource.PropertyMap, options plugin.ConstructOptions) (plugin.ConstructResult, error) { 3019 3020 urn, _, _, err := monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{ 3021 Parent: parent, 3022 Protect: options.Protect, 3023 Aliases: options.Aliases, 3024 Dependencies: options.Dependencies, 3025 }) 3026 assert.NoError(t, err) 3027 3028 urnB, idB, _, err = monitor.RegisterResource("pkgA:m:typB", "resB", true, deploytest.ResourceOptions{ 3029 Parent: urn, 3030 Inputs: resource.PropertyMap{ 3031 "bar": resource.NewStringProperty("baz"), 3032 }, 3033 }) 3034 assert.NoError(t, err) 3035 3036 return plugin.ConstructResult{ 3037 URN: urn, 3038 Outputs: resource.PropertyMap{ 3039 "foo": resource.NewStringProperty("bar"), 3040 "res": resource.MakeCustomResourceReference(urnB, idB, ""), 3041 }, 3042 }, nil 3043 } 3044 3045 return &deploytest.Provider{ 3046 CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64, 3047 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 3048 return "created-id", inputs, resource.StatusOK, nil 3049 }, 3050 ReadF: func(urn resource.URN, id resource.ID, 3051 inputs, state resource.PropertyMap) (plugin.ReadResult, resource.Status, error) { 3052 return plugin.ReadResult{Inputs: inputs, Outputs: state}, resource.StatusOK, nil 3053 }, 3054 ConstructF: construct, 3055 }, nil 3056 }), 3057 } 3058 3059 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3060 _, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{ 3061 Remote: true, 3062 }) 3063 assert.NoError(t, err) 3064 assert.Equal(t, resource.PropertyMap{ 3065 "foo": resource.NewStringProperty("bar"), 3066 "res": resource.MakeCustomResourceReference(urnB, idB, ""), 3067 }, state) 3068 3069 result, _, err := monitor.Invoke("pulumi:pulumi:getResource", resource.PropertyMap{ 3070 "urn": resource.NewStringProperty(string(urnB)), 3071 }, "", "") 3072 assert.NoError(t, err) 3073 assert.Equal(t, resource.PropertyMap{ 3074 "urn": resource.NewStringProperty(string(urnB)), 3075 "id": resource.NewStringProperty(string(idB)), 3076 "state": resource.NewObjectProperty(resource.PropertyMap{ 3077 "bar": resource.NewStringProperty("baz"), 3078 }), 3079 }, result) 3080 return nil 3081 }) 3082 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3083 3084 p := &TestPlan{ 3085 Options: UpdateOptions{Host: host}, 3086 Steps: MakeBasicLifecycleSteps(t, 4), 3087 } 3088 p.Run(t, nil) 3089 } 3090 3091 func TestConfigSecrets(t *testing.T) { 3092 t.Parallel() 3093 3094 loaders := []*deploytest.ProviderLoader{ 3095 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3096 return &deploytest.Provider{}, nil 3097 }), 3098 } 3099 3100 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3101 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true) 3102 assert.NoError(t, err) 3103 return nil 3104 }) 3105 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3106 3107 crypter := config.NewSymmetricCrypter(make([]byte, 32)) 3108 secret, err := crypter.EncryptValue(context.Background(), "hunter2") 3109 assert.NoError(t, err) 3110 3111 p := &TestPlan{ 3112 Options: UpdateOptions{Host: host}, 3113 Steps: MakeBasicLifecycleSteps(t, 2), 3114 Config: config.Map{ 3115 config.MustMakeKey("pkgA", "secret"): config.NewSecureValue(secret), 3116 }, 3117 Decrypter: crypter, 3118 } 3119 3120 project := p.GetProject() 3121 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 3122 assert.Nil(t, res) 3123 3124 if !assert.Len(t, snap.Resources, 2) { 3125 return 3126 } 3127 3128 provider := snap.Resources[0] 3129 assert.True(t, provider.Inputs["secret"].IsSecret()) 3130 assert.True(t, provider.Outputs["secret"].IsSecret()) 3131 } 3132 3133 func TestComponentOutputs(t *testing.T) { 3134 t.Parallel() 3135 3136 // A component's outputs should never be returned by `RegisterResource`, even if (especially if) there are 3137 // outputs from a prior deployment and the component's inputs have not changed. 3138 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3139 urn, _, state, err := monitor.RegisterResource("component", "resA", false) 3140 assert.NoError(t, err) 3141 assert.Equal(t, resource.PropertyMap{}, state) 3142 3143 err = monitor.RegisterResourceOutputs(urn, resource.PropertyMap{ 3144 "foo": resource.NewStringProperty("bar"), 3145 }) 3146 assert.NoError(t, err) 3147 return nil 3148 }) 3149 host := deploytest.NewPluginHost(nil, nil, program) 3150 3151 p := &TestPlan{ 3152 Options: UpdateOptions{Host: host}, 3153 Steps: MakeBasicLifecycleSteps(t, 1), 3154 } 3155 p.Run(t, nil) 3156 } 3157 3158 // Test calling a method. 3159 func TestSingleComponentMethodDefaultProviderLifecycle(t *testing.T) { 3160 t.Parallel() 3161 3162 var urn resource.URN 3163 3164 loaders := []*deploytest.ProviderLoader{ 3165 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3166 construct := func(monitor *deploytest.ResourceMonitor, 3167 typ, name string, parent resource.URN, inputs resource.PropertyMap, 3168 options plugin.ConstructOptions) (plugin.ConstructResult, error) { 3169 3170 var err error 3171 urn, _, _, err = monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{ 3172 Parent: parent, 3173 Aliases: options.Aliases, 3174 Protect: options.Protect, 3175 }) 3176 assert.NoError(t, err) 3177 3178 _, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{ 3179 Parent: urn, 3180 }) 3181 assert.NoError(t, err) 3182 3183 outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")} 3184 err = monitor.RegisterResourceOutputs(urn, outs) 3185 assert.NoError(t, err) 3186 3187 return plugin.ConstructResult{ 3188 URN: urn, 3189 Outputs: outs, 3190 }, nil 3191 } 3192 3193 call := func(monitor *deploytest.ResourceMonitor, tok tokens.ModuleMember, args resource.PropertyMap, 3194 info plugin.CallInfo, options plugin.CallOptions) (plugin.CallResult, error) { 3195 3196 assert.Equal(t, resource.PropertyMap{ 3197 "name": resource.NewStringProperty("Alice"), 3198 }, args) 3199 name := args["name"].StringValue() 3200 3201 result, _, err := monitor.Invoke("pulumi:pulumi:getResource", resource.PropertyMap{ 3202 "urn": resource.NewStringProperty(string(urn)), 3203 }, "", "") 3204 assert.NoError(t, err) 3205 state := result["state"] 3206 foo := state.ObjectValue()["foo"].StringValue() 3207 3208 message := fmt.Sprintf("%s, %s!", name, foo) 3209 return plugin.CallResult{ 3210 Return: resource.PropertyMap{ 3211 "message": resource.NewStringProperty(message), 3212 }, 3213 }, nil 3214 } 3215 3216 return &deploytest.Provider{ 3217 ConstructF: construct, 3218 CallF: call, 3219 }, nil 3220 }), 3221 } 3222 3223 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3224 _, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{ 3225 Remote: true, 3226 }) 3227 assert.NoError(t, err) 3228 assert.Equal(t, resource.PropertyMap{ 3229 "foo": resource.NewStringProperty("bar"), 3230 }, state) 3231 3232 outs, _, _, err := monitor.Call("pkgA:m:typA/methodA", resource.PropertyMap{ 3233 "name": resource.NewStringProperty("Alice"), 3234 }, "", "") 3235 assert.NoError(t, err) 3236 assert.Equal(t, resource.PropertyMap{ 3237 "message": resource.NewStringProperty("Alice, bar!"), 3238 }, outs) 3239 3240 return nil 3241 }) 3242 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3243 3244 p := &TestPlan{ 3245 Options: UpdateOptions{Host: host}, 3246 Steps: MakeBasicLifecycleSteps(t, 4), 3247 } 3248 p.Run(t, nil) 3249 } 3250 3251 // Test creating a resource from a method. 3252 func TestSingleComponentMethodResourceDefaultProviderLifecycle(t *testing.T) { 3253 t.Parallel() 3254 3255 var urn resource.URN 3256 3257 loaders := []*deploytest.ProviderLoader{ 3258 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3259 construct := func(monitor *deploytest.ResourceMonitor, 3260 typ, name string, parent resource.URN, inputs resource.PropertyMap, 3261 options plugin.ConstructOptions) (plugin.ConstructResult, error) { 3262 3263 var err error 3264 urn, _, _, err = monitor.RegisterResource(tokens.Type(typ), name, false, deploytest.ResourceOptions{ 3265 Parent: parent, 3266 Aliases: options.Aliases, 3267 Protect: options.Protect, 3268 }) 3269 assert.NoError(t, err) 3270 3271 _, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resA", true, deploytest.ResourceOptions{ 3272 Parent: urn, 3273 }) 3274 assert.NoError(t, err) 3275 3276 outs := resource.PropertyMap{"foo": resource.NewStringProperty("bar")} 3277 err = monitor.RegisterResourceOutputs(urn, outs) 3278 assert.NoError(t, err) 3279 3280 return plugin.ConstructResult{ 3281 URN: urn, 3282 Outputs: outs, 3283 }, nil 3284 } 3285 3286 call := func(monitor *deploytest.ResourceMonitor, tok tokens.ModuleMember, args resource.PropertyMap, 3287 info plugin.CallInfo, options plugin.CallOptions) (plugin.CallResult, error) { 3288 3289 _, _, _, err := monitor.RegisterResource("pkgA:m:typC", "resA", true, deploytest.ResourceOptions{ 3290 Parent: urn, 3291 }) 3292 assert.NoError(t, err) 3293 3294 return plugin.CallResult{}, nil 3295 } 3296 3297 return &deploytest.Provider{ 3298 ConstructF: construct, 3299 CallF: call, 3300 }, nil 3301 }), 3302 } 3303 3304 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3305 _, _, state, err := monitor.RegisterResource("pkgA:m:typA", "resA", false, deploytest.ResourceOptions{ 3306 Remote: true, 3307 }) 3308 assert.NoError(t, err) 3309 assert.Equal(t, resource.PropertyMap{ 3310 "foo": resource.NewStringProperty("bar"), 3311 }, state) 3312 3313 _, _, _, err = monitor.Call("pkgA:m:typA/methodA", resource.PropertyMap{}, "", "") 3314 assert.NoError(t, err) 3315 return nil 3316 }) 3317 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3318 3319 p := &TestPlan{ 3320 Options: UpdateOptions{Host: host}, 3321 Steps: MakeBasicLifecycleSteps(t, 4), 3322 } 3323 p.Run(t, nil) 3324 } 3325 3326 // This tests a scenario involving two remote components with interdependencies that are only represented in the 3327 // user program. 3328 func TestComponentDeleteDependencies(t *testing.T) { 3329 t.Parallel() 3330 3331 var ( 3332 firstURN resource.URN 3333 nestedURN resource.URN 3334 sgURN resource.URN 3335 secondURN resource.URN 3336 ruleURN resource.URN 3337 3338 err error 3339 ) 3340 3341 loaders := []*deploytest.ProviderLoader{ 3342 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3343 return &deploytest.Provider{}, nil 3344 }), 3345 deploytest.NewProviderLoader("pkgB", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3346 return &deploytest.Provider{ 3347 ConstructF: func(monitor *deploytest.ResourceMonitor, typ, name string, parent resource.URN, 3348 inputs resource.PropertyMap, options plugin.ConstructOptions) (plugin.ConstructResult, error) { 3349 3350 switch typ { 3351 case "pkgB:m:first": 3352 firstURN, _, _, err = monitor.RegisterResource("pkgB:m:first", name, false) 3353 require.NoError(t, err) 3354 3355 nestedURN, _, _, err = monitor.RegisterResource("nested", "nested", false, 3356 deploytest.ResourceOptions{ 3357 Parent: firstURN, 3358 }) 3359 require.NoError(t, err) 3360 3361 sgURN, _, _, err = monitor.RegisterResource("pkgA:m:sg", "sg", true, deploytest.ResourceOptions{ 3362 Parent: nestedURN, 3363 }) 3364 require.NoError(t, err) 3365 3366 err = monitor.RegisterResourceOutputs(nestedURN, resource.PropertyMap{}) 3367 require.NoError(t, err) 3368 3369 err = monitor.RegisterResourceOutputs(firstURN, resource.PropertyMap{}) 3370 require.NoError(t, err) 3371 3372 return plugin.ConstructResult{URN: firstURN}, nil 3373 case "pkgB:m:second": 3374 secondURN, _, _, err = monitor.RegisterResource("pkgB:m:second", name, false, 3375 deploytest.ResourceOptions{ 3376 Dependencies: options.Dependencies, 3377 }) 3378 require.NoError(t, err) 3379 3380 ruleURN, _, _, err = monitor.RegisterResource("pkgA:m:rule", "rule", true, 3381 deploytest.ResourceOptions{ 3382 Parent: secondURN, 3383 Dependencies: options.PropertyDependencies["sgID"], 3384 }) 3385 require.NoError(t, err) 3386 3387 err = monitor.RegisterResourceOutputs(secondURN, resource.PropertyMap{}) 3388 require.NoError(t, err) 3389 3390 return plugin.ConstructResult{URN: secondURN}, nil 3391 default: 3392 return plugin.ConstructResult{}, fmt.Errorf("unexpected type %v", typ) 3393 } 3394 }, 3395 }, nil 3396 }), 3397 } 3398 3399 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3400 _, _, _, err = monitor.RegisterResource("pkgB:m:first", "first", false, deploytest.ResourceOptions{ 3401 Remote: true, 3402 }) 3403 require.NoError(t, err) 3404 3405 _, _, _, err = monitor.RegisterResource("pkgB:m:second", "second", false, deploytest.ResourceOptions{ 3406 Remote: true, 3407 PropertyDeps: map[resource.PropertyKey][]resource.URN{ 3408 "sgID": {sgURN}, 3409 }, 3410 Dependencies: []resource.URN{firstURN}, 3411 }) 3412 require.NoError(t, err) 3413 3414 return nil 3415 }) 3416 3417 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3418 p := &TestPlan{Options: UpdateOptions{Host: host}} 3419 3420 p.Steps = []TestStep{ 3421 { 3422 Op: Update, 3423 SkipPreview: true, 3424 }, 3425 { 3426 Op: Destroy, 3427 SkipPreview: true, 3428 Validate: func(project workspace.Project, target deploy.Target, entries JournalEntries, 3429 evts []Event, res result.Result) result.Result { 3430 assert.Nil(t, res) 3431 3432 firstIndex, nestedIndex, sgIndex, secondIndex, ruleIndex := -1, -1, -1, -1, -1 3433 3434 for i, entry := range entries { 3435 switch urn := entry.Step.URN(); urn { 3436 case firstURN: 3437 firstIndex = i 3438 case nestedURN: 3439 nestedIndex = i 3440 case sgURN: 3441 sgIndex = i 3442 case secondURN: 3443 secondIndex = i 3444 case ruleURN: 3445 ruleIndex = i 3446 } 3447 } 3448 3449 assert.Less(t, ruleIndex, sgIndex) 3450 assert.Less(t, ruleIndex, secondIndex) 3451 assert.Less(t, secondIndex, firstIndex) 3452 assert.Less(t, secondIndex, sgIndex) 3453 assert.Less(t, sgIndex, nestedIndex) 3454 assert.Less(t, nestedIndex, firstIndex) 3455 3456 return res 3457 }, 3458 }, 3459 } 3460 p.Run(t, nil) 3461 } 3462 3463 func TestProtect(t *testing.T) { 3464 t.Parallel() 3465 3466 idCounter := 0 3467 deleteCounter := 0 3468 3469 loaders := []*deploytest.ProviderLoader{ 3470 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3471 return &deploytest.Provider{ 3472 DiffF: func( 3473 urn resource.URN, 3474 id resource.ID, 3475 olds, news resource.PropertyMap, 3476 ignoreChanges []string) (plugin.DiffResult, error) { 3477 if !olds["foo"].DeepEquals(news["foo"]) { 3478 // If foo changes do a replace, we use this to check we don't delete on replace 3479 return plugin.DiffResult{ 3480 Changes: plugin.DiffSome, 3481 ReplaceKeys: []resource.PropertyKey{"foo"}, 3482 }, nil 3483 } 3484 return plugin.DiffResult{}, nil 3485 }, 3486 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 3487 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 3488 resourceID := resource.ID(fmt.Sprintf("created-id-%d", idCounter)) 3489 idCounter = idCounter + 1 3490 return resourceID, news, resource.StatusOK, nil 3491 }, 3492 DeleteF: func(urn resource.URN, id resource.ID, olds resource.PropertyMap, 3493 timeout float64) (resource.Status, error) { 3494 deleteCounter = deleteCounter + 1 3495 return resource.StatusOK, nil 3496 }, 3497 }, nil 3498 }, deploytest.WithoutGrpc), 3499 } 3500 3501 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 3502 "foo": "bar", 3503 }) 3504 3505 shouldProtect := true 3506 createResource := true 3507 3508 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3509 3510 if createResource { 3511 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 3512 Inputs: ins, 3513 Protect: shouldProtect, 3514 }) 3515 assert.NoError(t, err) 3516 } 3517 3518 return nil 3519 }) 3520 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3521 3522 p := &TestPlan{ 3523 Options: UpdateOptions{Host: host}, 3524 } 3525 3526 project := p.GetProject() 3527 3528 // Run an update to create the resource 3529 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 3530 assert.Nil(t, res) 3531 assert.NotNil(t, snap) 3532 assert.Len(t, snap.Resources, 2) 3533 assert.Equal(t, "created-id-0", snap.Resources[1].ID.String()) 3534 assert.Equal(t, 0, deleteCounter) 3535 3536 expectedUrn := snap.Resources[1].URN 3537 expectedMessage := "" 3538 3539 // Both updates below should give a diagnostic event 3540 validate := func(project workspace.Project, 3541 target deploy.Target, entries JournalEntries, 3542 events []Event, res result.Result) result.Result { 3543 for _, event := range events { 3544 if event.Type == DiagEvent { 3545 payload := event.Payload().(DiagEventPayload) 3546 assert.Equal(t, expectedUrn, payload.URN) 3547 assert.Equal(t, expectedMessage, payload.Message) 3548 break 3549 } 3550 } 3551 return res 3552 } 3553 3554 // Run a new update which will cause a replace, we should get an error 3555 expectedMessage = "<{%reset%}>unable to replace resource \"urn:pulumi:test::test::pkgA:m:typA::resA\"\n" + 3556 "as it is currently marked for protection. To unprotect the resource, remove the `protect` flag from " + 3557 "the resource in your Pulumi program and run `pulumi up`<{%reset%}>\n" 3558 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 3559 "foo": "baz", 3560 }) 3561 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 3562 assert.NotNil(t, res) 3563 assert.NotNil(t, snap) 3564 assert.Len(t, snap.Resources, 2) 3565 assert.Equal(t, "created-id-0", snap.Resources[1].ID.String()) 3566 assert.Equal(t, 0, deleteCounter) 3567 3568 // Run a new update which will cause a delete, we still shouldn't see a provider delete 3569 expectedMessage = "<{%reset%}>unable to delete resource \"urn:pulumi:test::test::pkgA:m:typA::resA\"\n" + 3570 "as it is currently marked for protection. To unprotect the resource, either remove the `protect` flag " + 3571 "from the resource in your Pulumi program and run `pulumi up` or use the command:\n" + 3572 "`pulumi state unprotect 'urn:pulumi:test::test::pkgA:m:typA::resA'`<{%reset%}>\n" 3573 createResource = false 3574 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 3575 assert.NotNil(t, res) 3576 assert.NotNil(t, snap) 3577 assert.Len(t, snap.Resources, 2) 3578 assert.Equal(t, "created-id-0", snap.Resources[1].ID.String()) 3579 assert.Equal(t, true, snap.Resources[1].Protect) 3580 assert.Equal(t, 0, deleteCounter) 3581 3582 // Run a new update to remove the protect and replace in the same update, this should delete the old one 3583 // and create the new one 3584 createResource = true 3585 shouldProtect = false 3586 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3587 assert.Nil(t, res) 3588 assert.NotNil(t, snap) 3589 assert.Len(t, snap.Resources, 2) 3590 assert.Equal(t, "created-id-1", snap.Resources[1].ID.String()) 3591 assert.Equal(t, false, snap.Resources[1].Protect) 3592 assert.Equal(t, 1, deleteCounter) 3593 3594 // Run a new update to add the protect flag, nothing else should change 3595 shouldProtect = true 3596 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3597 assert.Nil(t, res) 3598 assert.NotNil(t, snap) 3599 assert.Len(t, snap.Resources, 2) 3600 assert.Equal(t, "created-id-1", snap.Resources[1].ID.String()) 3601 assert.Equal(t, true, snap.Resources[1].Protect) 3602 assert.Equal(t, 1, deleteCounter) 3603 3604 // Edit the snapshot to remove the protect flag and try and replace 3605 snap.Resources[1].Protect = false 3606 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 3607 "foo": "daz", 3608 }) 3609 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 3610 assert.Nil(t, res) 3611 assert.NotNil(t, snap) 3612 assert.Len(t, snap.Resources, 2) 3613 assert.Equal(t, "created-id-2", snap.Resources[1].ID.String()) 3614 assert.Equal(t, 2, deleteCounter) 3615 } 3616 3617 func TestRetainOnDelete(t *testing.T) { 3618 t.Parallel() 3619 3620 idCounter := 0 3621 3622 loaders := []*deploytest.ProviderLoader{ 3623 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3624 return &deploytest.Provider{ 3625 DiffF: func( 3626 urn resource.URN, 3627 id resource.ID, 3628 olds, news resource.PropertyMap, 3629 ignoreChanges []string) (plugin.DiffResult, error) { 3630 if !olds["foo"].DeepEquals(news["foo"]) { 3631 // If foo changes do a replace, we use this to check we don't delete on replace 3632 return plugin.DiffResult{ 3633 Changes: plugin.DiffSome, 3634 ReplaceKeys: []resource.PropertyKey{"foo"}, 3635 }, nil 3636 } 3637 return plugin.DiffResult{}, nil 3638 }, 3639 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 3640 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 3641 resourceID := resource.ID(fmt.Sprintf("created-id-%d", idCounter)) 3642 idCounter = idCounter + 1 3643 return resourceID, news, resource.StatusOK, nil 3644 }, 3645 DeleteF: func(urn resource.URN, id resource.ID, olds resource.PropertyMap, 3646 timeout float64) (resource.Status, error) { 3647 assert.Fail(t, "Delete was called") 3648 return resource.StatusOK, nil 3649 }, 3650 }, nil 3651 }, deploytest.WithoutGrpc), 3652 } 3653 3654 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 3655 "foo": "bar", 3656 }) 3657 3658 createResource := true 3659 3660 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3661 3662 if createResource { 3663 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 3664 Inputs: ins, 3665 RetainOnDelete: true, 3666 }) 3667 assert.NoError(t, err) 3668 } 3669 3670 return nil 3671 }) 3672 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3673 3674 p := &TestPlan{ 3675 Options: UpdateOptions{Host: host}, 3676 } 3677 3678 project := p.GetProject() 3679 3680 // Run an update to create the resource 3681 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 3682 assert.Nil(t, res) 3683 assert.NotNil(t, snap) 3684 assert.Len(t, snap.Resources, 2) 3685 assert.Equal(t, "created-id-0", snap.Resources[1].ID.String()) 3686 3687 // Run a new update which will cause a replace, we shouldn't see a provider delete but should get a new id 3688 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 3689 "foo": "baz", 3690 }) 3691 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3692 assert.Nil(t, res) 3693 assert.NotNil(t, snap) 3694 assert.Len(t, snap.Resources, 2) 3695 assert.Equal(t, "created-id-1", snap.Resources[1].ID.String()) 3696 3697 // Run a new update which will cause a delete, we still shouldn't see a provider delete 3698 createResource = false 3699 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3700 assert.Nil(t, res) 3701 assert.NotNil(t, snap) 3702 assert.Len(t, snap.Resources, 0) 3703 } 3704 3705 func TestDeletedWith(t *testing.T) { 3706 t.Parallel() 3707 3708 idCounter := 0 3709 3710 topURN := resource.URN("") 3711 3712 loaders := []*deploytest.ProviderLoader{ 3713 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3714 return &deploytest.Provider{ 3715 DiffF: func( 3716 urn resource.URN, 3717 id resource.ID, 3718 olds, news resource.PropertyMap, 3719 ignoreChanges []string) (plugin.DiffResult, error) { 3720 if !olds["foo"].DeepEquals(news["foo"]) { 3721 // If foo changes do a replace, we use this to check we don't delete on replace 3722 return plugin.DiffResult{ 3723 Changes: plugin.DiffSome, 3724 ReplaceKeys: []resource.PropertyKey{"foo"}, 3725 }, nil 3726 } 3727 return plugin.DiffResult{}, nil 3728 }, 3729 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 3730 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 3731 resourceID := resource.ID(fmt.Sprintf("created-id-%d", idCounter)) 3732 idCounter = idCounter + 1 3733 return resourceID, news, resource.StatusOK, nil 3734 }, 3735 DeleteF: func(urn resource.URN, id resource.ID, olds resource.PropertyMap, 3736 timeout float64) (resource.Status, error) { 3737 if urn != topURN { 3738 // Only topURN (aURN) should be actually deleted 3739 assert.Fail(t, "Delete was called") 3740 } 3741 return resource.StatusOK, nil 3742 }, 3743 }, nil 3744 }, deploytest.WithoutGrpc), 3745 } 3746 3747 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 3748 "foo": "bar", 3749 }) 3750 3751 createResource := true 3752 3753 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3754 3755 if createResource { 3756 aURN, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 3757 Inputs: ins, 3758 }) 3759 assert.NoError(t, err) 3760 topURN = aURN 3761 3762 bURN, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 3763 Inputs: ins, 3764 DeletedWith: aURN, 3765 }) 3766 assert.NoError(t, err) 3767 3768 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{ 3769 Inputs: ins, 3770 DeletedWith: bURN, 3771 }) 3772 assert.NoError(t, err) 3773 } 3774 3775 return nil 3776 }) 3777 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3778 3779 p := &TestPlan{ 3780 Options: UpdateOptions{Host: host}, 3781 } 3782 3783 project := p.GetProject() 3784 3785 // Run an update to create the resource 3786 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 3787 assert.Nil(t, res) 3788 assert.NotNil(t, snap) 3789 assert.Len(t, snap.Resources, 4) 3790 assert.Equal(t, "created-id-0", snap.Resources[1].ID.String()) 3791 assert.Equal(t, "created-id-1", snap.Resources[2].ID.String()) 3792 assert.Equal(t, "created-id-2", snap.Resources[3].ID.String()) 3793 3794 // Run a new update which will cause a replace, we should only see a provider delete for aURN but should 3795 // get a new id for everything 3796 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 3797 "foo": "baz", 3798 }) 3799 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3800 assert.Nil(t, res) 3801 assert.NotNil(t, snap) 3802 assert.Len(t, snap.Resources, 4) 3803 assert.Equal(t, "created-id-3", snap.Resources[1].ID.String()) 3804 assert.Equal(t, "created-id-4", snap.Resources[2].ID.String()) 3805 assert.Equal(t, "created-id-5", snap.Resources[3].ID.String()) 3806 3807 // Run a new update which will cause a delete, we still shouldn't see a provider delete for anything but aURN 3808 createResource = false 3809 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3810 assert.Nil(t, res) 3811 assert.NotNil(t, snap) 3812 assert.Len(t, snap.Resources, 0) 3813 } 3814 3815 func TestDeletedWithCircularDependency(t *testing.T) { 3816 // This test should be removed if DeletedWith circular dependency is taken care of. 3817 // At the mean time, if there is a circular dependency - none shall be deleted. 3818 t.Parallel() 3819 3820 idCounter := 0 3821 3822 loaders := []*deploytest.ProviderLoader{ 3823 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3824 return &deploytest.Provider{ 3825 DiffF: func( 3826 urn resource.URN, 3827 id resource.ID, 3828 olds, news resource.PropertyMap, 3829 ignoreChanges []string) (plugin.DiffResult, error) { 3830 return plugin.DiffResult{}, nil 3831 }, 3832 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 3833 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 3834 resourceID := resource.ID(fmt.Sprintf("created-id-%d", idCounter)) 3835 idCounter = idCounter + 1 3836 return resourceID, news, resource.StatusOK, nil 3837 }, 3838 DeleteF: func(urn resource.URN, id resource.ID, olds resource.PropertyMap, 3839 timeout float64) (resource.Status, error) { 3840 3841 assert.Fail(t, "Delete was called") 3842 3843 return resource.StatusOK, nil 3844 }, 3845 }, nil 3846 }, deploytest.WithoutGrpc), 3847 } 3848 3849 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 3850 "foo": "bar", 3851 }) 3852 3853 createResource := true 3854 cURN := resource.URN("") 3855 3856 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3857 3858 if createResource { 3859 aURN, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 3860 Inputs: ins, 3861 DeletedWith: cURN, 3862 }) 3863 assert.NoError(t, err) 3864 3865 bURN, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 3866 Inputs: ins, 3867 DeletedWith: aURN, 3868 }) 3869 assert.NoError(t, err) 3870 3871 cURN, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resC", true, deploytest.ResourceOptions{ 3872 Inputs: ins, 3873 DeletedWith: bURN, 3874 }) 3875 assert.NoError(t, err) 3876 } 3877 3878 return nil 3879 }) 3880 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3881 3882 p := &TestPlan{ 3883 Options: UpdateOptions{Host: host}, 3884 } 3885 3886 project := p.GetProject() 3887 3888 // Run an update to create the resource 3889 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 3890 assert.Nil(t, res) 3891 assert.NotNil(t, snap) 3892 assert.Len(t, snap.Resources, 4) 3893 assert.Equal(t, "created-id-0", snap.Resources[1].ID.String()) 3894 assert.Equal(t, "created-id-1", snap.Resources[2].ID.String()) 3895 assert.Equal(t, "created-id-2", snap.Resources[3].ID.String()) 3896 3897 // Run again to update DeleteWith for resA 3898 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3899 assert.Nil(t, res) 3900 assert.NotNil(t, snap) 3901 assert.Len(t, snap.Resources, 4) 3902 assert.Equal(t, "created-id-0", snap.Resources[1].ID.String()) 3903 assert.Equal(t, "created-id-1", snap.Resources[2].ID.String()) 3904 assert.Equal(t, "created-id-2", snap.Resources[3].ID.String()) 3905 3906 // Run a new update which will cause a delete, we still shouldn't see a provider delete 3907 createResource = false 3908 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 3909 assert.Nil(t, res) 3910 assert.NotNil(t, snap) 3911 assert.Len(t, snap.Resources, 0) 3912 } 3913 3914 func TestInvalidGetIDReportsUserError(t *testing.T) { 3915 t.Parallel() 3916 3917 loaders := []*deploytest.ProviderLoader{ 3918 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3919 return &deploytest.Provider{}, nil 3920 }, deploytest.WithoutGrpc), 3921 } 3922 3923 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3924 _, _, err := monitor.ReadResource("pkgA:m:typA", "resA", "", "", resource.PropertyMap{}, "", "") 3925 assert.Error(t, err) 3926 return nil 3927 }) 3928 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3929 3930 p := &TestPlan{ 3931 Options: UpdateOptions{Host: host}, 3932 } 3933 3934 project := p.GetProject() 3935 3936 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 3937 "<{%reset%}>Expected an ID for urn:pulumi:test::test::pkgA:m:typA::resA<{%reset%}>")) 3938 3939 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 3940 assert.Nil(t, res) 3941 assert.NotNil(t, snap) 3942 assert.Len(t, snap.Resources, 1) 3943 } 3944 3945 func TestEventSecrets(t *testing.T) { 3946 t.Parallel() 3947 3948 loaders := []*deploytest.ProviderLoader{ 3949 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 3950 return &deploytest.Provider{ 3951 DiffF: func(urn resource.URN, id resource.ID, 3952 olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 3953 3954 diff := olds.Diff(news) 3955 if diff == nil { 3956 return plugin.DiffResult{Changes: plugin.DiffNone}, nil 3957 } 3958 detailedDiff := plugin.NewDetailedDiffFromObjectDiff(diff) 3959 changedKeys := diff.ChangedKeys() 3960 3961 return plugin.DiffResult{ 3962 Changes: plugin.DiffSome, 3963 ChangedKeys: changedKeys, 3964 DetailedDiff: detailedDiff, 3965 }, nil 3966 }, 3967 3968 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 3969 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 3970 return news, resource.StatusOK, nil 3971 }, 3972 CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64, 3973 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 3974 return resource.ID("id123"), inputs, resource.StatusOK, nil 3975 }, 3976 }, nil 3977 }), 3978 } 3979 3980 var inputs resource.PropertyMap 3981 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 3982 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 3983 Inputs: inputs, 3984 }) 3985 assert.NoError(t, err) 3986 return nil 3987 }) 3988 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 3989 3990 p := &TestPlan{ 3991 Options: UpdateOptions{Host: host}, 3992 Steps: []TestStep{{ 3993 Op: Update, 3994 SkipPreview: true, 3995 }}, 3996 } 3997 3998 inputs = resource.PropertyMap{ 3999 "webhooks": resource.MakeSecret(resource.NewArrayProperty([]resource.PropertyValue{ 4000 resource.NewObjectProperty(resource.PropertyMap{ 4001 "clientConfig": resource.NewObjectProperty(resource.PropertyMap{ 4002 "service": resource.NewStringProperty("foo"), 4003 }), 4004 }), 4005 })), 4006 } 4007 snap := p.Run(t, nil) 4008 4009 inputs = resource.PropertyMap{ 4010 "webhooks": resource.MakeSecret(resource.NewArrayProperty([]resource.PropertyValue{ 4011 resource.NewObjectProperty(resource.PropertyMap{ 4012 "clientConfig": resource.NewObjectProperty(resource.PropertyMap{ 4013 "service": resource.NewStringProperty("bar"), 4014 }), 4015 }), 4016 })), 4017 } 4018 p.Steps[0].Validate = func(project workspace.Project, target deploy.Target, entries JournalEntries, 4019 evts []Event, res result.Result) result.Result { 4020 4021 for _, e := range evts { 4022 var step StepEventMetadata 4023 switch e.Type { 4024 case ResourcePreEvent: 4025 step = e.Payload().(ResourcePreEventPayload).Metadata 4026 case ResourceOutputsEvent: 4027 step = e.Payload().(ResourceOutputsEventPayload).Metadata 4028 default: 4029 continue 4030 } 4031 if step.URN.Name() != "resA" { 4032 continue 4033 } 4034 4035 assert.True(t, step.Old.Inputs["webhooks"].IsSecret()) 4036 assert.True(t, step.Old.Outputs["webhooks"].IsSecret()) 4037 assert.True(t, step.New.Inputs["webhooks"].IsSecret()) 4038 } 4039 return res 4040 } 4041 p.Run(t, snap) 4042 } 4043 4044 func TestAdditionalSecretOutputs(t *testing.T) { 4045 t.Parallel() 4046 4047 t.Skip("AdditionalSecretOutputs warning is currently disabled") 4048 4049 loaders := []*deploytest.ProviderLoader{ 4050 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 4051 return &deploytest.Provider{ 4052 CreateF: func(urn resource.URN, inputs resource.PropertyMap, timeout float64, 4053 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 4054 return resource.ID("id123"), inputs, resource.StatusOK, nil 4055 }, 4056 }, nil 4057 }), 4058 } 4059 4060 var inputs resource.PropertyMap 4061 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 4062 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 4063 Inputs: inputs, 4064 AdditionalSecretOutputs: []resource.PropertyKey{"a", "b"}, 4065 }) 4066 assert.NoError(t, err) 4067 return nil 4068 }) 4069 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 4070 4071 p := &TestPlan{ 4072 Options: UpdateOptions{Host: host}, 4073 } 4074 4075 project := p.GetProject() 4076 4077 inputs = resource.PropertyMap{ 4078 "a": resource.NewStringProperty("testA"), 4079 // b is missing 4080 "c": resource.MakeSecret(resource.NewStringProperty("testC")), 4081 } 4082 4083 // Run an update to create the resource and check we warn about b 4084 validate := func( 4085 project workspace.Project, target deploy.Target, 4086 entries JournalEntries, events []Event, 4087 res result.Result) result.Result { 4088 4089 if res != nil { 4090 return res 4091 } 4092 4093 for i := range events { 4094 if events[i].Type == "diag" { 4095 payload := events[i].Payload().(engine.DiagEventPayload) 4096 if payload.Severity == "warning" && 4097 payload.URN == "urn:pulumi:test::test::pkgA:m:typA::resA" && 4098 payload.Message == "<{%reset%}>Could not find property 'b' listed in additional secret outputs.<{%reset%}>\n" { 4099 // Found the message we expected 4100 return nil 4101 } 4102 } 4103 } 4104 return result.Error("Expected a diagnostic message, got none") 4105 } 4106 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 4107 assert.Nil(t, res) 4108 4109 // Should have the provider and resA 4110 assert.Len(t, snap.Resources, 2) 4111 resA := snap.Resources[1] 4112 assert.Equal(t, []resource.PropertyKey{"a", "b"}, resA.AdditionalSecretOutputs) 4113 assert.True(t, resA.Outputs["a"].IsSecret()) 4114 assert.True(t, resA.Outputs["c"].IsSecret()) 4115 } 4116 4117 func TestDefaultParents(t *testing.T) { 4118 t.Parallel() 4119 t.Skipf("Default parents disabled due to https://github.com/pulumi/pulumi/issues/10950") 4120 4121 loaders := []*deploytest.ProviderLoader{ 4122 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 4123 return &deploytest.Provider{ 4124 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 4125 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 4126 return "created-id", news, resource.StatusOK, nil 4127 }, 4128 }, nil 4129 }, deploytest.WithoutGrpc), 4130 } 4131 4132 program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 4133 _, _, _, err := monitor.RegisterResource( 4134 resource.RootStackType, 4135 info.Project+"-"+info.Stack, 4136 false, 4137 deploytest.ResourceOptions{}) 4138 assert.NoError(t, err) 4139 4140 _, _, _, err = monitor.RegisterResource( 4141 "pkgA:m:typA", 4142 "resA", 4143 true, 4144 deploytest.ResourceOptions{}) 4145 assert.NoError(t, err) 4146 4147 return nil 4148 }) 4149 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 4150 4151 p := &TestPlan{ 4152 Options: UpdateOptions{Host: host}, 4153 } 4154 4155 project := p.GetProject() 4156 4157 // Run an update to create the resource 4158 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 4159 assert.Nil(t, res) 4160 assert.NotNil(t, snap) 4161 assert.Len(t, snap.Resources, 3) 4162 4163 // Assert that resource 0 is the stack 4164 assert.Equal(t, resource.RootStackType, snap.Resources[0].Type) 4165 // Assert that the other 2 resources have the stack as a parent 4166 assert.Equal(t, snap.Resources[0].URN, snap.Resources[1].Parent) 4167 assert.Equal(t, snap.Resources[0].URN, snap.Resources[2].Parent) 4168 } 4169 4170 func TestPendingDeleteOrder(t *testing.T) { 4171 // Test for https://github.com/pulumi/pulumi/issues/2948 Ensure that if we have resources A and B, and we 4172 // go to replace A but then fail to replace B that we correctly handle everything in the same order when 4173 // we retry the update. 4174 // 4175 // That is normally for this operation we would do the following: 4176 // 1. Create new A 4177 // 2. Create new B 4178 // 3. Delete old B 4179 // 4. Delete old A 4180 // So if step 2 fails to create the new B we want to see: 4181 // 1. Create new A 4182 // 2. Create new B (fail) 4183 // 1. Create new B 4184 // 2. Delete old B 4185 // 3. Delete old A 4186 // Currently (and what #2948 tracks) is that the engine does the following: 4187 // 1. Create new A 4188 // 2. Create new B (fail) 4189 // 3. Delete old A 4190 // 1. Create new B 4191 // 2. Delete old B 4192 // That delete A fails because the delete B needs to happen first. 4193 4194 t.Parallel() 4195 4196 cloudState := map[resource.ID]resource.PropertyMap{} 4197 4198 failCreationOfTypB := false 4199 4200 loaders := []*deploytest.ProviderLoader{ 4201 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 4202 return &deploytest.Provider{ 4203 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 4204 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 4205 4206 if strings.Contains(string(urn), "typB") && failCreationOfTypB { 4207 return "", nil, resource.StatusOK, fmt.Errorf("Could not create typB") 4208 } 4209 4210 id := resource.ID(fmt.Sprintf("%d", len(cloudState))) 4211 if !preview { 4212 cloudState[id] = news 4213 } 4214 return id, news, resource.StatusOK, nil 4215 }, 4216 DeleteF: func(urn resource.URN, 4217 id resource.ID, olds resource.PropertyMap, timeout float64) (resource.Status, error) { 4218 // Fail if anything in cloud state still points to us 4219 for _, res := range cloudState { 4220 for _, v := range res { 4221 if v.IsString() && v.StringValue() == string(id) { 4222 return resource.StatusOK, fmt.Errorf("Can not delete %s", id) 4223 } 4224 } 4225 } 4226 4227 delete(cloudState, id) 4228 return resource.StatusOK, nil 4229 }, 4230 DiffF: func(urn resource.URN, 4231 id resource.ID, olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 4232 if strings.Contains(string(urn), "typA") { 4233 if !olds["foo"].DeepEquals(news["foo"]) { 4234 return plugin.DiffResult{ 4235 Changes: plugin.DiffSome, 4236 ReplaceKeys: []resource.PropertyKey{"foo"}, 4237 DetailedDiff: map[string]plugin.PropertyDiff{ 4238 "foo": { 4239 Kind: plugin.DiffUpdateReplace, 4240 InputDiff: true, 4241 }, 4242 }, 4243 DeleteBeforeReplace: false, 4244 }, nil 4245 } 4246 } 4247 if strings.Contains(string(urn), "typB") { 4248 if !olds["parent"].DeepEquals(news["parent"]) { 4249 return plugin.DiffResult{ 4250 Changes: plugin.DiffSome, 4251 ReplaceKeys: []resource.PropertyKey{"parent"}, 4252 DetailedDiff: map[string]plugin.PropertyDiff{ 4253 "parent": { 4254 Kind: plugin.DiffUpdateReplace, 4255 InputDiff: true, 4256 }, 4257 }, 4258 DeleteBeforeReplace: false, 4259 }, nil 4260 } 4261 } 4262 4263 return plugin.DiffResult{}, nil 4264 }, 4265 UpdateF: func(urn resource.URN, 4266 id resource.ID, olds, news resource.PropertyMap, timeout float64, 4267 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 4268 assert.Fail(t, "Didn't expect update to be called") 4269 return nil, resource.StatusOK, nil 4270 }, 4271 }, nil 4272 }, deploytest.WithoutGrpc), 4273 } 4274 4275 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 4276 "foo": "bar", 4277 }) 4278 program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 4279 _, idA, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 4280 Inputs: ins, 4281 }) 4282 assert.NoError(t, err) 4283 4284 _, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resB", true, deploytest.ResourceOptions{ 4285 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 4286 "parent": idA, 4287 }), 4288 }) 4289 assert.NoError(t, err) 4290 4291 return nil 4292 }) 4293 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 4294 4295 p := &TestPlan{ 4296 Options: UpdateOptions{Host: host}, 4297 } 4298 4299 project := p.GetProject() 4300 4301 // Run an update to create the resources 4302 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 4303 assert.Nil(t, res) 4304 assert.NotNil(t, snap) 4305 assert.Len(t, snap.Resources, 3) 4306 4307 // Trigger a replacement of A but fail to create B 4308 failCreationOfTypB = true 4309 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 4310 "foo": "baz", 4311 }) 4312 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 4313 // Assert that this fails, we should have two copies of A now, one new one and one old one pending delete 4314 assert.NotNil(t, res) 4315 assert.NotNil(t, snap) 4316 assert.Len(t, snap.Resources, 4) 4317 assert.Equal(t, snap.Resources[1].Type, tokens.Type("pkgA:m:typA")) 4318 assert.False(t, snap.Resources[1].Delete) 4319 assert.Equal(t, snap.Resources[2].Type, tokens.Type("pkgA:m:typA")) 4320 assert.True(t, snap.Resources[2].Delete) 4321 4322 // Now allow B to create and try again 4323 failCreationOfTypB = false 4324 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 4325 assert.Nil(t, res) 4326 assert.NotNil(t, snap) 4327 assert.Len(t, snap.Resources, 3) 4328 } 4329 4330 func TestDuplicatesDueToAliases(t *testing.T) { 4331 t.Parallel() 4332 4333 // This is a test for https://github.com/pulumi/pulumi/issues/11173 4334 // to check that we don't allow resource aliases to refer to other resources. 4335 // That is if you have A, then try and add B saying it's alias is A we should error that's a duplicate. 4336 // We need to be careful that we handle this regardless of the order we send the RegisterResource requests for A and B. 4337 4338 loaders := []*deploytest.ProviderLoader{ 4339 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 4340 return &deploytest.Provider{ 4341 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 4342 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 4343 return "created-id", news, resource.StatusOK, nil 4344 }, 4345 }, nil 4346 }, deploytest.WithoutGrpc), 4347 } 4348 4349 mode := 0 4350 program := deploytest.NewLanguageRuntime(func(info plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 4351 4352 switch mode { 4353 case 0: 4354 // Default case, just make resA 4355 _, _, _, err := monitor.RegisterResource( 4356 "pkgA:m:typA", 4357 "resA", 4358 true, 4359 deploytest.ResourceOptions{}) 4360 assert.NoError(t, err) 4361 4362 case 1: 4363 // First test case, try and create a new B that aliases to A. First make the A like normal... 4364 _, _, _, err := monitor.RegisterResource( 4365 "pkgA:m:typA", 4366 "resA", 4367 true, 4368 deploytest.ResourceOptions{}) 4369 assert.NoError(t, err) 4370 4371 // ... then make B with an alias, it should error 4372 _, _, _, err = monitor.RegisterResource( 4373 "pkgA:m:typA", 4374 "resB", 4375 true, 4376 deploytest.ResourceOptions{ 4377 Aliases: []resource.Alias{{Name: "resA"}}, 4378 }) 4379 assert.Error(t, err) 4380 4381 case 2: 4382 // Second test case, try and create a new B that aliases to A. First make the B with an alias... 4383 _, _, _, err := monitor.RegisterResource( 4384 "pkgA:m:typA", 4385 "resB", 4386 true, 4387 deploytest.ResourceOptions{ 4388 Aliases: []resource.Alias{{Name: "resA"}}, 4389 }) 4390 assert.NoError(t, err) 4391 4392 // ... then try to make the A like normal. It should error that it's already been aliased away 4393 _, _, _, err = monitor.RegisterResource( 4394 "pkgA:m:typA", 4395 "resA", 4396 true, 4397 deploytest.ResourceOptions{}) 4398 assert.Error(t, err) 4399 } 4400 return nil 4401 }) 4402 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 4403 4404 p := &TestPlan{ 4405 Options: UpdateOptions{Host: host}, 4406 } 4407 4408 project := p.GetProject() 4409 4410 // Run an update to create the starting A resource 4411 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 4412 assert.Nil(t, res) 4413 assert.NotNil(t, snap) 4414 assert.Len(t, snap.Resources, 2) 4415 assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA"), snap.Resources[1].URN) 4416 4417 // Set mode to try and create A then a B that aliases to it, this should fail 4418 mode = 1 4419 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 4420 assert.NotNil(t, res) 4421 assert.NotNil(t, snap) 4422 assert.Len(t, snap.Resources, 2) 4423 assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA"), snap.Resources[1].URN) 4424 4425 // Set mode to try and create B first then a A, this should fail 4426 mode = 2 4427 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 4428 assert.NotNil(t, res) 4429 assert.NotNil(t, snap) 4430 assert.Len(t, snap.Resources, 2) 4431 // Because we made the B first that's what should end up in the state file 4432 assert.Equal(t, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resB"), snap.Resources[1].URN) 4433 }