github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/engine/lifecycletest/update_plan_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 "regexp" 20 "strings" 21 "testing" 22 23 "github.com/blang/semver" 24 25 "github.com/stretchr/testify/assert" 26 27 . "github.com/pulumi/pulumi/pkg/v3/engine" 28 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 29 "github.com/pulumi/pulumi/pkg/v3/resource/deploy/deploytest" 30 "github.com/pulumi/pulumi/sdk/v3/go/common/display" 31 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 32 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" 33 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 34 ) 35 36 func TestPlannedUpdate(t *testing.T) { 37 t.Parallel() 38 39 loaders := []*deploytest.ProviderLoader{ 40 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 41 return &deploytest.Provider{ 42 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 43 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 44 return "created-id", news, resource.StatusOK, nil 45 }, 46 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 47 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 48 return news, resource.StatusOK, nil 49 }, 50 }, nil 51 }), 52 } 53 54 var ins resource.PropertyMap 55 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 56 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 57 Inputs: ins, 58 }) 59 assert.NoError(t, err) 60 return nil 61 }) 62 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 63 64 p := &TestPlan{ 65 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 66 } 67 68 project := p.GetProject() 69 70 // Generate a plan. 71 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 72 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 73 "foo": "bar", 74 "baz": map[string]interface{}{ 75 "a": 42, 76 "b": computed, 77 }, 78 "qux": []interface{}{ 79 computed, 80 24, 81 }, 82 "zed": computed, 83 }) 84 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 85 assert.Nil(t, res) 86 87 // Attempt to run an update using the plan. 88 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 89 "qux": []interface{}{ 90 "alpha", 91 24, 92 }, 93 }) 94 p.Options.Plan = plan.Clone() 95 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 96 "<{%reset%}>resource urn:pulumi:test::test::pkgA:m:typA::resA violates plan: "+ 97 "properties changed: +-baz[{map[a:{42} b:output<string>{}]}], +-foo[{bar}]<{%reset%}>\n")) 98 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 99 assert.Nil(t, res) 100 101 // Check the resource's state. 102 if !assert.Len(t, snap.Resources, 1) { 103 return 104 } 105 106 // Change the provider's planned operation to a same step. 107 // Remove the provider from the plan. 108 plan.ResourcePlans["urn:pulumi:test::test::pulumi:providers:pkgA::default"].Ops = []display.StepOp{deploy.OpSame} 109 110 // Attempt to run an update using the plan. 111 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 112 "foo": "bar", 113 "baz": map[string]interface{}{ 114 "a": 42, 115 "b": "alpha", 116 }, 117 "qux": []interface{}{ 118 "beta", 119 24, 120 }, 121 "zed": "grr", 122 }) 123 p.Options.Plan = plan.Clone() 124 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 125 assert.Nil(t, res) 126 127 // Check the resource's state. 128 if !assert.Len(t, snap.Resources, 2) { 129 return 130 } 131 132 expected := resource.NewPropertyMapFromMap(map[string]interface{}{ 133 "foo": "bar", 134 "baz": map[string]interface{}{ 135 "a": 42, 136 "b": "alpha", 137 }, 138 "qux": []interface{}{ 139 "beta", 140 24, 141 }, 142 "zed": "grr", 143 }) 144 assert.Equal(t, expected, snap.Resources[1].Outputs) 145 } 146 147 func TestUnplannedCreate(t *testing.T) { 148 t.Parallel() 149 150 loaders := []*deploytest.ProviderLoader{ 151 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 152 return &deploytest.Provider{ 153 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 154 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 155 return "created-id", news, resource.StatusOK, nil 156 }, 157 }, nil 158 }), 159 } 160 161 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 162 "foo": "bar", 163 }) 164 createResource := false 165 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 166 if createResource { 167 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 168 Inputs: ins, 169 }) 170 assert.NoError(t, err) 171 } 172 return nil 173 }) 174 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 175 176 p := &TestPlan{ 177 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 178 } 179 180 project := p.GetProject() 181 182 // Create a plan to do nothing 183 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 184 assert.Nil(t, res) 185 186 // Now set the flag for the language runtime to create a resource, and run update with the plan 187 createResource = true 188 p.Options.Plan = plan.Clone() 189 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 190 "<{%reset%}>create is not allowed by the plan: no steps were expected for this resource<{%reset%}>\n")) 191 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 192 assert.Nil(t, res) 193 194 // Check nothing was was created 195 assert.NotNil(t, snap) 196 if !assert.Len(t, snap.Resources, 0) { 197 return 198 } 199 } 200 201 func TestUnplannedDelete(t *testing.T) { 202 t.Parallel() 203 204 loaders := []*deploytest.ProviderLoader{ 205 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 206 return &deploytest.Provider{ 207 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 208 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 209 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 210 }, 211 DeleteF: func( 212 urn resource.URN, 213 id resource.ID, 214 olds resource.PropertyMap, 215 timeout float64) (resource.Status, error) { 216 return resource.StatusOK, nil 217 }, 218 }, nil 219 }), 220 } 221 222 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 223 "foo": "bar", 224 }) 225 createAllResources := true 226 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 227 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 228 Inputs: ins, 229 }) 230 assert.NoError(t, err) 231 232 if createAllResources { 233 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 234 Inputs: ins, 235 }) 236 assert.NoError(t, err) 237 } 238 239 return nil 240 }) 241 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 242 243 p := &TestPlan{ 244 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 245 } 246 247 project := p.GetProject() 248 249 // Create an initial snapshot that resA and resB exist 250 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 251 assert.Nil(t, res) 252 253 // Create a plan that resA and resB won't change 254 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 255 assert.Nil(t, res) 256 257 // Now set the flag for the language runtime to not create resB and run an update with 258 // the no-op plan, this should block the delete 259 createAllResources = false 260 p.Options.Plan = plan.Clone() 261 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 262 "<{%reset%}>delete is not allowed by the plan: this resource is constrained to same<{%reset%}>\n")) 263 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 264 assert.NotNil(t, snap) 265 assert.Nil(t, res) 266 267 // Check both resources and the provider are still listed in the snapshot 268 if !assert.Len(t, snap.Resources, 3) { 269 return 270 } 271 } 272 273 func TestExpectedDelete(t *testing.T) { 274 t.Parallel() 275 276 loaders := []*deploytest.ProviderLoader{ 277 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 278 return &deploytest.Provider{ 279 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 280 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 281 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 282 }, 283 DeleteF: func( 284 urn resource.URN, 285 id resource.ID, 286 olds resource.PropertyMap, 287 timeout float64) (resource.Status, error) { 288 return resource.StatusOK, nil 289 }, 290 }, nil 291 }), 292 } 293 294 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 295 "foo": "bar", 296 }) 297 createAllResources := true 298 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 299 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 300 Inputs: ins, 301 }) 302 assert.NoError(t, err) 303 304 if createAllResources { 305 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 306 Inputs: ins, 307 }) 308 assert.NoError(t, err) 309 } 310 311 return nil 312 }) 313 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 314 315 p := &TestPlan{ 316 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 317 } 318 319 project := p.GetProject() 320 321 // Create an initial snapshot that resA and resB exist 322 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 323 assert.NotNil(t, snap) 324 assert.Nil(t, res) 325 326 // Create a plan that resA is same and resB is deleted 327 createAllResources = false 328 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 329 assert.NotNil(t, plan) 330 assert.Nil(t, res) 331 332 // Now run but set the runtime to return resA and resB, given we expected resB to be deleted 333 // this should be an error 334 createAllResources = true 335 p.Options.Plan = plan.Clone() 336 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 337 "<{%reset%}>resource urn:pulumi:test::test::pkgA:m:typA::resB violates plan: "+ 338 "resource unexpectedly not deleted<{%reset%}>\n")) 339 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 340 assert.NotNil(t, snap) 341 assert.Nil(t, res) 342 343 // Check both resources and the provider are still listed in the snapshot 344 if !assert.Len(t, snap.Resources, 3) { 345 return 346 } 347 } 348 349 func TestExpectedCreate(t *testing.T) { 350 t.Parallel() 351 352 loaders := []*deploytest.ProviderLoader{ 353 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 354 return &deploytest.Provider{ 355 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 356 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 357 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 358 }, 359 }, nil 360 }), 361 } 362 363 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 364 "foo": "bar", 365 }) 366 createAllResources := false 367 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 368 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 369 Inputs: ins, 370 }) 371 assert.NoError(t, err) 372 373 if createAllResources { 374 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 375 Inputs: ins, 376 }) 377 assert.NoError(t, err) 378 } 379 380 return nil 381 }) 382 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 383 384 p := &TestPlan{ 385 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 386 } 387 388 project := p.GetProject() 389 390 // Create an initial snapshot that resA exists 391 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 392 assert.NotNil(t, snap) 393 assert.Nil(t, res) 394 395 // Create a plan that resA is same and resB is created 396 createAllResources = true 397 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 398 assert.NotNil(t, plan) 399 assert.Nil(t, res) 400 401 // Now run but set the runtime to return resA, given we expected resB to be created 402 // this should be an error 403 createAllResources = false 404 p.Options.Plan = plan.Clone() 405 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 406 "<{%reset%}>expected resource operations for "+ 407 "urn:pulumi:test::test::pkgA:m:typA::resB but none were seen<{%reset%}>\n")) 408 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 409 assert.NotNil(t, snap) 410 assert.Nil(t, res) 411 412 // Check resA and the provider are still listed in the snapshot 413 if !assert.Len(t, snap.Resources, 2) { 414 return 415 } 416 } 417 418 func TestPropertySetChange(t *testing.T) { 419 t.Parallel() 420 421 loaders := []*deploytest.ProviderLoader{ 422 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 423 return &deploytest.Provider{ 424 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 425 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 426 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 427 }, 428 }, nil 429 }), 430 } 431 432 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 433 "foo": "bar", 434 "frob": "baz", 435 }) 436 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 437 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 438 Inputs: ins, 439 }) 440 assert.NoError(t, err) 441 442 return nil 443 }) 444 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 445 446 p := &TestPlan{ 447 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 448 } 449 450 project := p.GetProject() 451 452 // Create an initial plan to create resA 453 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 454 assert.NotNil(t, plan) 455 assert.Nil(t, res) 456 457 // Now change the runtime to not return property "frob", this should error 458 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 459 "foo": "bar", 460 }) 461 p.Options.Plan = plan.Clone() 462 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 463 "<{%reset%}>resource urn:pulumi:test::test::pkgA:m:typA::resA violates plan: "+ 464 "properties changed: +-frob[{baz}]<{%reset%}>\n")) 465 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 466 assert.NotNil(t, snap) 467 assert.Nil(t, res) 468 } 469 470 func TestExpectedUnneededCreate(t *testing.T) { 471 t.Parallel() 472 473 loaders := []*deploytest.ProviderLoader{ 474 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 475 return &deploytest.Provider{ 476 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 477 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 478 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 479 }, 480 }, nil 481 }), 482 } 483 484 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 485 "foo": "bar", 486 }) 487 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 488 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 489 Inputs: ins, 490 }) 491 assert.NoError(t, err) 492 493 return nil 494 }) 495 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 496 497 p := &TestPlan{ 498 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 499 } 500 501 project := p.GetProject() 502 503 // Create a plan that resA needs creating 504 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 505 assert.NotNil(t, plan) 506 assert.Nil(t, res) 507 508 // Create an a snapshot that resA exists 509 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 510 assert.NotNil(t, snap) 511 assert.Nil(t, res) 512 513 // Now run again with the plan set but the snapshot that resA already exists 514 p.Options.Plan = plan.Clone() 515 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 516 assert.NotNil(t, snap) 517 assert.Nil(t, res) 518 519 // Check resA and the provider are still listed in the snapshot 520 if !assert.Len(t, snap.Resources, 2) { 521 return 522 } 523 } 524 525 func TestExpectedUnneededDelete(t *testing.T) { 526 t.Parallel() 527 528 loaders := []*deploytest.ProviderLoader{ 529 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 530 return &deploytest.Provider{ 531 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 532 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 533 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 534 }, 535 DeleteF: func( 536 urn resource.URN, 537 id resource.ID, 538 olds resource.PropertyMap, 539 timeout float64) (resource.Status, error) { 540 return resource.StatusOK, nil 541 }, 542 }, nil 543 }), 544 } 545 546 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 547 "foo": "bar", 548 }) 549 createResource := true 550 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 551 if createResource { 552 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 553 Inputs: ins, 554 }) 555 assert.NoError(t, err) 556 } 557 558 return nil 559 }) 560 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 561 562 p := &TestPlan{ 563 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 564 } 565 566 project := p.GetProject() 567 568 // Create an initial snapshot that resA exists 569 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 570 assert.Nil(t, res) 571 572 // Create a plan that resA is deleted 573 createResource = false 574 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 575 assert.Nil(t, res) 576 577 // Now run to delete resA 578 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 579 assert.NotNil(t, snap) 580 assert.Nil(t, res) 581 582 // Now run again with the plan set but the snapshot that resA is already deleted 583 p.Options.Plan = plan.Clone() 584 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 585 assert.NotNil(t, snap) 586 assert.Nil(t, res) 587 588 // Check the resources are still gone 589 if !assert.Len(t, snap.Resources, 0) { 590 return 591 } 592 } 593 594 func TestResoucesWithSames(t *testing.T) { 595 t.Parallel() 596 597 // This test checks that if between generating a constraint and running the update that if new resources have been 598 // added to the stack that the update doesn't change those resources in any way that they don't cause constraint 599 // errors. 600 601 loaders := []*deploytest.ProviderLoader{ 602 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 603 return &deploytest.Provider{ 604 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 605 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 606 return "created-id", news, resource.StatusOK, nil 607 }, 608 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 609 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 610 return news, resource.StatusOK, nil 611 }, 612 }, nil 613 }), 614 } 615 616 var ins resource.PropertyMap 617 createA := false 618 createB := false 619 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 620 if createA { 621 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 622 Inputs: ins, 623 }) 624 assert.NoError(t, err) 625 } 626 627 if createB { 628 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 629 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 630 "X": "Y", 631 }), 632 }) 633 assert.NoError(t, err) 634 } 635 return nil 636 }) 637 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 638 639 p := &TestPlan{ 640 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 641 } 642 643 project := p.GetProject() 644 645 // Generate a plan to create A 646 createA = true 647 createB = false 648 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 649 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 650 "foo": "bar", 651 "zed": computed, 652 }) 653 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 654 assert.Nil(t, res) 655 656 // Run an update that creates B 657 createA = false 658 createB = true 659 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 660 assert.Nil(t, res) 661 662 // Check the resource's state. 663 if !assert.Len(t, snap.Resources, 2) { 664 return 665 } 666 667 expected := resource.NewPropertyMapFromMap(map[string]interface{}{ 668 "X": "Y", 669 }) 670 assert.Equal(t, expected, snap.Resources[1].Outputs) 671 672 // Attempt to run an update with the plan on the stack that creates A and sames B 673 createA = true 674 createB = true 675 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 676 "foo": "bar", 677 "zed": 24, 678 }) 679 p.Options.Plan = plan.Clone() 680 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 681 assert.Nil(t, res) 682 683 // Check the resource's state. 684 if !assert.Len(t, snap.Resources, 3) { 685 return 686 } 687 688 expected = resource.NewPropertyMapFromMap(map[string]interface{}{ 689 "X": "Y", 690 }) 691 assert.Equal(t, expected, snap.Resources[2].Outputs) 692 693 expected = resource.NewPropertyMapFromMap(map[string]interface{}{ 694 "foo": "bar", 695 "zed": 24, 696 }) 697 assert.Equal(t, expected, snap.Resources[1].Outputs) 698 } 699 700 func TestPlannedPreviews(t *testing.T) { 701 t.Parallel() 702 703 // This checks that plans work in previews, this is very similar to TestPlannedUpdate except we only do previews 704 705 loaders := []*deploytest.ProviderLoader{ 706 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 707 return &deploytest.Provider{ 708 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 709 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 710 return "created-id", news, resource.StatusOK, nil 711 }, 712 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 713 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 714 return news, resource.StatusOK, nil 715 }, 716 }, nil 717 }), 718 } 719 720 var ins resource.PropertyMap 721 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 722 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 723 Inputs: ins, 724 }) 725 assert.NoError(t, err) 726 return nil 727 }) 728 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 729 730 p := &TestPlan{ 731 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 732 } 733 734 project := p.GetProject() 735 736 // Generate a plan. 737 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 738 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 739 "foo": "bar", 740 "baz": map[string]interface{}{ 741 "a": 42, 742 "b": computed, 743 }, 744 "qux": []interface{}{ 745 computed, 746 24, 747 }, 748 "zed": computed, 749 }) 750 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 751 assert.Nil(t, res) 752 753 // Attempt to run a new preview using the plan, given we've changed the property set this should fail 754 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 755 "qux": []interface{}{ 756 "alpha", 757 24, 758 }, 759 }) 760 p.Options.Plan = plan.Clone() 761 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 762 "<{%reset%}>resource urn:pulumi:test::test::pkgA:m:typA::resA violates plan: properties changed: "+ 763 "+-baz[{map[a:{42} b:output<string>{}]}], +-foo[{bar}]<{%reset%}>\n")) 764 _, res = TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, validate) 765 assert.Nil(t, res) 766 767 // Attempt to run an preview using the plan, such that the property set is now valid 768 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 769 "foo": "bar", 770 "baz": map[string]interface{}{ 771 "a": 42, 772 "b": computed, 773 }, 774 "qux": []interface{}{ 775 "beta", 776 24, 777 }, 778 "zed": "grr", 779 }) 780 p.Options.Plan = plan.Clone() 781 _, res = TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 782 assert.Nil(t, res) 783 } 784 785 func TestPlannedUpdateChangedStack(t *testing.T) { 786 t.Parallel() 787 788 // This tests the case that we run a planned update against a stack that has changed between preview and update 789 790 loaders := []*deploytest.ProviderLoader{ 791 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 792 return &deploytest.Provider{ 793 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 794 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 795 return "created-id", news, resource.StatusOK, nil 796 }, 797 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 798 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 799 return news, resource.StatusOK, nil 800 }, 801 }, nil 802 }), 803 } 804 805 var ins resource.PropertyMap 806 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 807 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 808 Inputs: ins, 809 }) 810 assert.NoError(t, err) 811 return nil 812 }) 813 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 814 815 p := &TestPlan{ 816 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 817 } 818 819 project := p.GetProject() 820 821 // Set initial data for foo and zed 822 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 823 "foo": "bar", 824 "zed": 24, 825 }) 826 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 827 assert.Nil(t, res) 828 829 // Generate a plan that we want to change foo 830 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 831 "foo": "baz", 832 "zed": 24, 833 }) 834 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 835 assert.Nil(t, res) 836 837 // Change zed in the stack 838 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 839 "foo": "bar", 840 "zed": 26, 841 }) 842 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 843 assert.Nil(t, res) 844 845 // Attempt to run an update using the plan but where we haven't updated our program for the change of zed 846 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 847 "foo": "baz", 848 "zed": 24, 849 }) 850 p.Options.Plan = plan.Clone() 851 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 852 "<{%reset%}>resource urn:pulumi:test::test::pkgA:m:typA::resA violates plan: "+ 853 "properties changed: =~zed[{24}]<{%reset%}>\n")) 854 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 855 assert.Nil(t, res) 856 857 // Check the resource's state we shouldn't of changed anything because the update failed 858 if !assert.Len(t, snap.Resources, 2) { 859 return 860 } 861 862 expected := resource.NewPropertyMapFromMap(map[string]interface{}{ 863 "foo": "bar", 864 "zed": 26, 865 }) 866 assert.Equal(t, expected, snap.Resources[1].Outputs) 867 } 868 869 func TestPlannedOutputChanges(t *testing.T) { 870 t.Parallel() 871 872 loaders := []*deploytest.ProviderLoader{ 873 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 874 return &deploytest.Provider{ 875 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 876 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 877 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 878 }, 879 }, nil 880 }), 881 } 882 883 outs := resource.NewPropertyMapFromMap(map[string]interface{}{ 884 "foo": "bar", 885 "frob": "baz", 886 }) 887 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 888 urn, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{}) 889 assert.NoError(t, err) 890 891 err = monitor.RegisterResourceOutputs(urn, outs) 892 assert.NoError(t, err) 893 894 return nil 895 }) 896 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 897 898 p := &TestPlan{ 899 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 900 } 901 902 project := p.GetProject() 903 904 // Create an initial plan to create resA and the outputs 905 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 906 assert.NotNil(t, plan) 907 assert.Nil(t, res) 908 909 // Now change the runtime to not return property "frob", this should error 910 outs = resource.NewPropertyMapFromMap(map[string]interface{}{ 911 "foo": "bar", 912 }) 913 p.Options.Plan = plan.Clone() 914 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 915 "<{%reset%}>resource violates plan: properties changed: +-frob[{baz}]<{%reset%}>\n")) 916 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 917 assert.NotNil(t, snap) 918 assert.Nil(t, res) 919 } 920 921 func TestPlannedInputOutputDifferences(t *testing.T) { 922 t.Parallel() 923 924 // This tests that plans are working on the program inputs, not the provider outputs 925 926 createOutputs := resource.NewPropertyMapFromMap(map[string]interface{}{ 927 "foo": "bar", 928 "frob": "baz", 929 "baz": 24, 930 }) 931 updateOutputs := resource.NewPropertyMapFromMap(map[string]interface{}{ 932 "foo": "bar", 933 "frob": "newBazzer", 934 "baz": 24, 935 }) 936 937 loaders := []*deploytest.ProviderLoader{ 938 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 939 return &deploytest.Provider{ 940 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 941 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 942 return resource.ID("created-id-" + urn.Name()), createOutputs, resource.StatusOK, nil 943 }, 944 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 945 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 946 return updateOutputs, resource.StatusOK, nil 947 }, 948 }, nil 949 }), 950 } 951 952 inputs := resource.NewPropertyMapFromMap(map[string]interface{}{ 953 "foo": "bar", 954 "frob": "baz", 955 }) 956 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 957 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 958 Inputs: inputs}) 959 assert.NoError(t, err) 960 961 return nil 962 }) 963 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 964 965 p := &TestPlan{ 966 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 967 } 968 969 project := p.GetProject() 970 971 // Create an initial plan to create resA 972 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 973 assert.NotNil(t, plan) 974 assert.Nil(t, res) 975 976 // Check we can create resA even though its outputs are different to the planned inputs 977 p.Options.Plan = plan.Clone() 978 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 979 assert.NotNil(t, snap) 980 assert.Nil(t, res) 981 982 // Make a plan to change resA 983 inputs = resource.NewPropertyMapFromMap(map[string]interface{}{ 984 "foo": "bar", 985 "frob": "newBazzer", 986 }) 987 p.Options.Plan = nil 988 plan, res = TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 989 assert.NotNil(t, plan) 990 assert.Nil(t, res) 991 992 // Test the plan fails if we don't pass newBazzer 993 inputs = resource.NewPropertyMapFromMap(map[string]interface{}{ 994 "foo": "bar", 995 "frob": "differentBazzer", 996 }) 997 p.Options.Plan = plan.Clone() 998 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 999 "<{%reset%}>resource urn:pulumi:test::test::pkgA:m:typA::resA violates plan: "+ 1000 "properties changed: ~~frob[{newBazzer}!={differentBazzer}]<{%reset%}>\n")) 1001 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, validate) 1002 assert.NotNil(t, snap) 1003 assert.Nil(t, res) 1004 1005 // Check the plan succeeds if we do pass newBazzer 1006 inputs = resource.NewPropertyMapFromMap(map[string]interface{}{ 1007 "foo": "bar", 1008 "frob": "newBazzer", 1009 }) 1010 p.Options.Plan = plan.Clone() 1011 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1012 assert.NotNil(t, snap) 1013 assert.Nil(t, res) 1014 } 1015 1016 func TestAliasWithPlans(t *testing.T) { 1017 t.Parallel() 1018 1019 // This tests that if a resource has an alias the plan for it is still used 1020 1021 loaders := []*deploytest.ProviderLoader{ 1022 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1023 return &deploytest.Provider{ 1024 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 1025 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 1026 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 1027 }, 1028 }, nil 1029 }), 1030 } 1031 1032 resourceName := "resA" 1033 var aliases []resource.URN 1034 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 1035 "foo": "bar", 1036 "frob": "baz", 1037 }) 1038 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1039 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", resourceName, true, deploytest.ResourceOptions{ 1040 Inputs: ins, 1041 AliasURNs: aliases, 1042 }) 1043 assert.NoError(t, err) 1044 1045 return nil 1046 }) 1047 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1048 1049 p := &TestPlan{ 1050 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 1051 } 1052 1053 project := p.GetProject() 1054 1055 // Create an initial ResA 1056 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 1057 assert.NotNil(t, snap) 1058 assert.Nil(t, res) 1059 1060 // Update the name and alias and make a plan for resA 1061 resourceName = "newResA" 1062 aliases = make([]resource.URN, 1) 1063 aliases[0] = resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA") 1064 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 1065 assert.NotNil(t, plan) 1066 assert.Nil(t, res) 1067 1068 // Now try and run with the plan 1069 p.Options.Plan = plan.Clone() 1070 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1071 assert.NotNil(t, snap) 1072 assert.Nil(t, res) 1073 } 1074 1075 func TestComputedCanBeDropped(t *testing.T) { 1076 t.Parallel() 1077 1078 // This tests that values that show as <computed> in the plan can be dropped in the update (because they may of 1079 // resolved to undefined). We're testing both RegisterResource and RegisterResourceOutputs here. 1080 1081 loaders := []*deploytest.ProviderLoader{ 1082 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1083 return &deploytest.Provider{ 1084 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 1085 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 1086 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 1087 }, 1088 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 1089 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 1090 return news, resource.StatusOK, nil 1091 }, 1092 }, nil 1093 }), 1094 } 1095 1096 var resourceInputs resource.PropertyMap 1097 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1098 urn, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{}) 1099 assert.NoError(t, err) 1100 1101 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 1102 Inputs: resourceInputs, 1103 }) 1104 assert.NoError(t, err) 1105 1106 // We're using the same property set on purpose, this is not a test bug 1107 err = monitor.RegisterResourceOutputs(urn, resourceInputs) 1108 assert.NoError(t, err) 1109 1110 return nil 1111 }) 1112 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1113 1114 p := &TestPlan{ 1115 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 1116 } 1117 1118 project := p.GetProject() 1119 1120 // The three property sets we'll use in this test 1121 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 1122 computedPropertySet := resource.NewPropertyMapFromMap(map[string]interface{}{ 1123 "foo": "bar", 1124 "baz": map[string]interface{}{ 1125 "a": 42, 1126 "b": computed, 1127 }, 1128 "qux": []interface{}{ 1129 computed, 1130 24, 1131 }, 1132 "zed": computed, 1133 }) 1134 fullPropertySet := resource.NewPropertyMapFromMap(map[string]interface{}{ 1135 "foo": "bar", 1136 "baz": map[string]interface{}{ 1137 "a": 42, 1138 "b": "alpha", 1139 }, 1140 "qux": []interface{}{ 1141 "beta", 1142 24, 1143 }, 1144 "zed": "grr", 1145 }) 1146 partialPropertySet := resource.NewPropertyMapFromMap(map[string]interface{}{ 1147 "foo": "bar", 1148 "baz": map[string]interface{}{ 1149 "a": 42, 1150 }, 1151 "qux": []interface{}{ 1152 nil, // computed values that resolve to undef don't get dropped from arrays, they just become null 1153 24, 1154 }, 1155 }) 1156 1157 // Generate a plan. 1158 resourceInputs = computedPropertySet 1159 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 1160 assert.Nil(t, res) 1161 1162 // Attempt to run an update using the plan with all computed values removed 1163 resourceInputs = partialPropertySet 1164 p.Options.Plan = plan.Clone() 1165 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 1166 assert.Nil(t, res) 1167 1168 // Check the resource's state. 1169 if !assert.Len(t, snap.Resources, 3) { 1170 return 1171 } 1172 1173 assert.Equal(t, partialPropertySet, snap.Resources[1].Outputs) 1174 assert.Equal(t, partialPropertySet, snap.Resources[2].Outputs) 1175 1176 // Now run an update to set the values of the computed properties... 1177 resourceInputs = fullPropertySet 1178 p.Options.Plan = nil 1179 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1180 assert.Nil(t, res) 1181 1182 // Check the resource's state. 1183 if !assert.Len(t, snap.Resources, 3) { 1184 return 1185 } 1186 1187 assert.Equal(t, fullPropertySet, snap.Resources[1].Outputs) 1188 assert.Equal(t, fullPropertySet, snap.Resources[2].Outputs) 1189 1190 // ...and then build a new plan where they're computed updates (vs above where its computed creates) 1191 resourceInputs = computedPropertySet 1192 plan, res = TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 1193 assert.Nil(t, res) 1194 1195 // Now run the an update with the plan and check the update is allowed to remove these properties 1196 resourceInputs = partialPropertySet 1197 p.Options.Plan = plan.Clone() 1198 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1199 assert.Nil(t, res) 1200 1201 // Check the resource's state. 1202 if !assert.Len(t, snap.Resources, 3) { 1203 return 1204 } 1205 1206 assert.Equal(t, partialPropertySet, snap.Resources[1].Outputs) 1207 assert.Equal(t, partialPropertySet, snap.Resources[2].Outputs) 1208 } 1209 1210 func TestPlannedUpdateWithNondeterministicCheck(t *testing.T) { 1211 t.Parallel() 1212 1213 loaders := []*deploytest.ProviderLoader{ 1214 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1215 return &deploytest.Provider{ 1216 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 1217 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 1218 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 1219 }, 1220 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 1221 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 1222 return news, resource.StatusOK, nil 1223 }, 1224 CheckF: func(urn resource.URN, 1225 olds, news resource.PropertyMap, _ []byte) (resource.PropertyMap, []plugin.CheckFailure, error) { 1226 1227 // If we have name use it, else use olds name, else make one up 1228 if _, has := news["name"]; has { 1229 return news, nil, nil 1230 } 1231 if _, has := olds["name"]; has { 1232 result := news.Copy() 1233 result["name"] = olds["name"] 1234 return result, nil, nil 1235 } 1236 1237 name, err := resource.NewUniqueHex(urn.Name().String(), 8, 512) 1238 assert.Nil(t, err) 1239 1240 result := news.Copy() 1241 result["name"] = resource.NewStringProperty(name) 1242 return result, nil, nil 1243 }, 1244 }, nil 1245 }), 1246 } 1247 1248 var ins resource.PropertyMap 1249 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1250 _, _, outs, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1251 Inputs: ins, 1252 }) 1253 assert.NoError(t, err) 1254 1255 _, _, _, err = monitor.RegisterResource("pkgA:m:typA", "resB", true, deploytest.ResourceOptions{ 1256 Inputs: resource.NewPropertyMapFromMap(map[string]interface{}{ 1257 "other": outs["name"].StringValue(), 1258 }), 1259 }) 1260 assert.NoError(t, err) 1261 1262 return nil 1263 }) 1264 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1265 1266 p := &TestPlan{ 1267 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 1268 } 1269 1270 project := p.GetProject() 1271 1272 // Generate a plan. 1273 computed := interface{}(resource.Computed{Element: resource.NewStringProperty("")}) 1274 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1275 "foo": "bar", 1276 "zed": computed, 1277 }) 1278 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 1279 assert.Nil(t, res) 1280 1281 // Attempt to run an update using the plan. 1282 // This should fail because of the non-determinism 1283 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1284 "foo": "bar", 1285 "zed": "baz", 1286 }) 1287 p.Options.Plan = plan.Clone() 1288 1289 validate := ExpectDiagMessage(t, 1290 "<{%reset%}>resource urn:pulumi:test::test::pkgA:m:typA::resA violates plan: "+ 1291 "properties changed: \\+\\+name\\[{res[\\d\\w]{9}}!={res[\\d\\w]{9}}\\]<{%reset%}>\\n") 1292 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 1293 assert.Nil(t, res) 1294 1295 // Check the resource's state. 1296 if !assert.Len(t, snap.Resources, 1) { 1297 return 1298 } 1299 } 1300 1301 func TestPlannedUpdateWithCheckFailure(t *testing.T) { 1302 // Regression test for https://github.com/pulumi/pulumi/issues/9247 1303 1304 t.Parallel() 1305 1306 loaders := []*deploytest.ProviderLoader{ 1307 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1308 return &deploytest.Provider{ 1309 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 1310 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 1311 return "created-id", news, resource.StatusOK, nil 1312 }, 1313 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 1314 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 1315 return news, resource.StatusOK, nil 1316 }, 1317 CheckF: func(urn resource.URN, olds, news resource.PropertyMap, 1318 randomSeed []byte) (resource.PropertyMap, []plugin.CheckFailure, error) { 1319 if news["foo"].StringValue() == "bad" { 1320 return nil, []plugin.CheckFailure{ 1321 {Property: resource.PropertyKey("foo"), Reason: "Bad foo"}, 1322 }, nil 1323 } 1324 return news, nil, nil 1325 }, 1326 }, nil 1327 }), 1328 } 1329 1330 var ins resource.PropertyMap 1331 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1332 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1333 Inputs: ins, 1334 }) 1335 assert.NoError(t, err) 1336 return nil 1337 }) 1338 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1339 1340 p := &TestPlan{ 1341 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 1342 } 1343 1344 project := p.GetProject() 1345 1346 // Generate a plan with bad inputs 1347 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1348 "foo": "bad", 1349 }) 1350 validate := ExpectDiagMessage(t, regexp.QuoteMeta( 1351 "<{%reset%}>pkgA:m:typA resource 'resA': property foo value {bad} has a problem: Bad foo<{%reset%}>\n")) 1352 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, validate) 1353 assert.Nil(t, plan) 1354 assert.Nil(t, res) 1355 1356 // Generate a plan with good inputs 1357 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1358 "foo": "good", 1359 }) 1360 plan, res = TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 1361 assert.NotNil(t, plan) 1362 assert.Contains(t, plan.ResourcePlans, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA")) 1363 assert.Nil(t, res) 1364 1365 // Try and run against the plan with inputs that will fail Check 1366 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1367 "foo": "bad", 1368 }) 1369 p.Options.Plan = plan.Clone() 1370 validate = ExpectDiagMessage(t, regexp.QuoteMeta( 1371 "<{%reset%}>pkgA:m:typA resource 'resA': property foo value {bad} has a problem: Bad foo<{%reset%}>\n")) 1372 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, validate) 1373 assert.Nil(t, res) 1374 assert.NotNil(t, snap) 1375 1376 // Check the resource's state. 1377 if !assert.Len(t, snap.Resources, 1) { 1378 return 1379 } 1380 } 1381 1382 func TestPluginsAreDownloaded(t *testing.T) { 1383 t.Parallel() 1384 1385 loaders := []*deploytest.ProviderLoader{ 1386 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1387 return &deploytest.Provider{}, nil 1388 }), 1389 } 1390 1391 semver10 := semver.MustParse("1.0.0") 1392 1393 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1394 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{}) 1395 assert.NoError(t, err) 1396 return nil 1397 }, workspace.PluginSpec{Name: "pkgA"}, workspace.PluginSpec{Name: "pkgB", Version: &semver10}) 1398 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1399 1400 p := &TestPlan{ 1401 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 1402 } 1403 1404 project := p.GetProject() 1405 1406 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 1407 assert.NotNil(t, plan) 1408 assert.Contains(t, plan.ResourcePlans, resource.URN("urn:pulumi:test::test::pkgA:m:typA::resA")) 1409 assert.Nil(t, res) 1410 } 1411 1412 func TestProviderDeterministicPreview(t *testing.T) { 1413 t.Parallel() 1414 1415 var generatedName resource.PropertyValue 1416 1417 loaders := []*deploytest.ProviderLoader{ 1418 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1419 return &deploytest.Provider{ 1420 CheckF: func( 1421 urn resource.URN, 1422 olds, news resource.PropertyMap, 1423 randomSeed []byte) (resource.PropertyMap, []plugin.CheckFailure, error) { 1424 // make a deterministic autoname 1425 if _, has := news["name"]; !has { 1426 if name, has := olds["name"]; has { 1427 news["name"] = name 1428 } else { 1429 name, err := resource.NewUniqueName(randomSeed, urn.Name().String(), -1, -1, nil) 1430 assert.Nil(t, err) 1431 generatedName = resource.NewStringProperty(name) 1432 news["name"] = generatedName 1433 } 1434 } 1435 1436 return news, nil, nil 1437 }, 1438 DiffF: func( 1439 urn resource.URN, 1440 id resource.ID, 1441 olds, news resource.PropertyMap, 1442 ignoreChanges []string) (plugin.DiffResult, error) { 1443 if !olds["foo"].DeepEquals(news["foo"]) { 1444 // If foo changes do a replace, we use this to check we get a new name 1445 return plugin.DiffResult{ 1446 Changes: plugin.DiffSome, 1447 ReplaceKeys: []resource.PropertyKey{"foo"}, 1448 }, nil 1449 } 1450 return plugin.DiffResult{}, nil 1451 }, 1452 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 1453 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 1454 return "created-id", news, resource.StatusOK, nil 1455 }, 1456 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 1457 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 1458 return news, resource.StatusOK, nil 1459 }, 1460 }, nil 1461 }, deploytest.WithoutGrpc), 1462 } 1463 1464 ins := resource.NewPropertyMapFromMap(map[string]interface{}{ 1465 "foo": "bar", 1466 }) 1467 1468 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1469 _, _, _, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1470 Inputs: ins, 1471 }) 1472 assert.NoError(t, err) 1473 return nil 1474 }) 1475 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1476 1477 p := &TestPlan{ 1478 Options: UpdateOptions{Host: host, GeneratePlan: true, Experimental: true}, 1479 } 1480 1481 project := p.GetProject() 1482 1483 // Run a preview, this should want to create resA with a given name 1484 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, nil), p.Options, p.BackendClient, nil) 1485 assert.Nil(t, res) 1486 assert.True(t, generatedName.IsString()) 1487 assert.NotEqual(t, "", generatedName.StringValue()) 1488 expectedName := generatedName 1489 1490 // Run an update, we should get the same name as we saw in preview 1491 p.Options.Plan = plan 1492 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 1493 assert.Nil(t, res) 1494 assert.NotNil(t, snap) 1495 assert.Len(t, snap.Resources, 2) 1496 assert.Equal(t, expectedName, snap.Resources[1].Inputs["name"]) 1497 assert.Equal(t, expectedName, snap.Resources[1].Outputs["name"]) 1498 1499 // Run a new update which will cause a replace and check we get a new name 1500 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1501 "foo": "baz", 1502 }) 1503 p.Options.Plan = nil 1504 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1505 assert.Nil(t, res) 1506 assert.NotNil(t, snap) 1507 assert.Len(t, snap.Resources, 2) 1508 assert.NotEqual(t, expectedName, snap.Resources[1].Inputs["name"]) 1509 assert.NotEqual(t, expectedName, snap.Resources[1].Outputs["name"]) 1510 } 1511 1512 func TestPlannedUpdateWithDependentDelete(t *testing.T) { 1513 t.Parallel() 1514 1515 var diffResult *plugin.DiffResult 1516 1517 loaders := []*deploytest.ProviderLoader{ 1518 deploytest.NewProviderLoader("pkgA", semver.MustParse("1.0.0"), func() (plugin.Provider, error) { 1519 return &deploytest.Provider{ 1520 CreateF: func(urn resource.URN, news resource.PropertyMap, timeout float64, 1521 preview bool) (resource.ID, resource.PropertyMap, resource.Status, error) { 1522 return resource.ID("created-id-" + urn.Name()), news, resource.StatusOK, nil 1523 }, 1524 UpdateF: func(urn resource.URN, id resource.ID, olds, news resource.PropertyMap, timeout float64, 1525 ignoreChanges []string, preview bool) (resource.PropertyMap, resource.Status, error) { 1526 return news, resource.StatusOK, nil 1527 }, 1528 CheckF: func(urn resource.URN, 1529 olds, news resource.PropertyMap, _ []byte) (resource.PropertyMap, []plugin.CheckFailure, error) { 1530 return news, nil, nil 1531 }, 1532 DiffF: func(urn resource.URN, 1533 id resource.ID, olds, news resource.PropertyMap, ignoreChanges []string) (plugin.DiffResult, error) { 1534 if strings.Contains(string(urn), "resA") || strings.Contains(string(urn), "resB") { 1535 assert.NotNil(t, diffResult, "Diff was called but diffResult wasn't set") 1536 return *diffResult, nil 1537 } 1538 return plugin.DiffResult{}, nil 1539 }, 1540 }, nil 1541 }), 1542 } 1543 1544 var ins resource.PropertyMap 1545 program := deploytest.NewLanguageRuntime(func(_ plugin.RunInfo, monitor *deploytest.ResourceMonitor) error { 1546 resA, _, outs, err := monitor.RegisterResource("pkgA:m:typA", "resA", true, deploytest.ResourceOptions{ 1547 Inputs: ins, 1548 }) 1549 assert.NoError(t, err) 1550 1551 _, _, _, err = monitor.RegisterResource("pkgA:m:typB", "resB", true, deploytest.ResourceOptions{ 1552 Inputs: outs, 1553 Dependencies: []resource.URN{resA}, 1554 }) 1555 assert.NoError(t, err) 1556 1557 return nil 1558 }) 1559 host := deploytest.NewPluginHost(nil, nil, program, loaders...) 1560 1561 p := &TestPlan{ 1562 Options: UpdateOptions{Host: host, GeneratePlan: true}, 1563 } 1564 1565 project := p.GetProject() 1566 1567 // Create an initial ResA and resB 1568 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1569 "foo": "bar", 1570 "zed": "baz", 1571 }) 1572 snap, res := TestOp(Update).Run(project, p.GetTarget(t, nil), p.Options, false, p.BackendClient, nil) 1573 assert.NotNil(t, snap) 1574 assert.Nil(t, res) 1575 1576 // Update the input and mark it as a replace, check that both A and B are marked as replacements 1577 ins = resource.NewPropertyMapFromMap(map[string]interface{}{ 1578 "foo": "frob", 1579 "zed": "baz", 1580 }) 1581 diffResult = &plugin.DiffResult{ 1582 Changes: plugin.DiffSome, 1583 ReplaceKeys: []resource.PropertyKey{"foo"}, 1584 StableKeys: []resource.PropertyKey{"zed"}, 1585 DetailedDiff: map[string]plugin.PropertyDiff{ 1586 "foo": { 1587 Kind: plugin.DiffUpdateReplace, 1588 InputDiff: true, 1589 }, 1590 }, 1591 DeleteBeforeReplace: true, 1592 } 1593 plan, res := TestOp(Update).Plan(project, p.GetTarget(t, snap), p.Options, p.BackendClient, nil) 1594 assert.NotNil(t, plan) 1595 assert.Nil(t, res) 1596 1597 assert.Equal(t, 3, len(plan.ResourcePlans["urn:pulumi:test::test::pkgA:m:typA::resA"].Ops)) 1598 assert.Equal(t, 3, len(plan.ResourcePlans["urn:pulumi:test::test::pkgA:m:typB::resB"].Ops)) 1599 1600 // Now try and run with the plan 1601 p.Options.Plan = plan.Clone() 1602 snap, res = TestOp(Update).Run(project, p.GetTarget(t, snap), p.Options, false, p.BackendClient, nil) 1603 assert.NotNil(t, snap) 1604 assert.Nil(t, res) 1605 }