github.com/crossplane/upjet@v1.3.0/pkg/controller/external_test.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package controller 6 7 import ( 8 "context" 9 "testing" 10 11 xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 12 "github.com/crossplane/crossplane-runtime/pkg/logging" 13 xpmeta "github.com/crossplane/crossplane-runtime/pkg/meta" 14 "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" 15 xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" 16 xpfake "github.com/crossplane/crossplane-runtime/pkg/resource/fake" 17 "github.com/crossplane/crossplane-runtime/pkg/test" 18 "github.com/google/go-cmp/cmp" 19 "github.com/google/go-cmp/cmp/cmpopts" 20 "github.com/pkg/errors" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "sigs.k8s.io/controller-runtime/pkg/client" 23 24 "github.com/crossplane/upjet/pkg/config" 25 "github.com/crossplane/upjet/pkg/resource" 26 "github.com/crossplane/upjet/pkg/resource/fake" 27 "github.com/crossplane/upjet/pkg/resource/json" 28 "github.com/crossplane/upjet/pkg/terraform" 29 ) 30 31 const ( 32 testPath = "test/path" 33 ) 34 35 var ( 36 errBoom = errors.New("boom") 37 exampleState = &json.StateV4{ 38 Resources: []json.ResourceStateV4{ 39 { 40 Instances: []json.InstanceObjectStateV4{ 41 { 42 AttributesRaw: []byte(`{"id":"some-id","obs":"obsval","param":"paramval"}`), 43 }, 44 }, 45 }, 46 }, 47 } 48 exampleCriticalAnnotations = map[string]string{ 49 resource.AnnotationKeyPrivateRawAttribute: "", 50 xpmeta.AnnotationKeyExternalName: "some-id", 51 } 52 ) 53 54 type WorkspaceFns struct { 55 ApplyAsyncFn func(callback terraform.CallbackFn) error 56 ApplyFn func(ctx context.Context) (terraform.ApplyResult, error) 57 DestroyAsyncFn func(callback terraform.CallbackFn) error 58 DestroyFn func(ctx context.Context) error 59 RefreshFn func(ctx context.Context) (terraform.RefreshResult, error) 60 ImportFn func(ctx context.Context, tr resource.Terraformed) (terraform.ImportResult, error) 61 PlanFn func(ctx context.Context) (terraform.PlanResult, error) 62 } 63 64 func (c WorkspaceFns) ApplyAsync(callback terraform.CallbackFn) error { 65 return c.ApplyAsyncFn(callback) 66 } 67 68 func (c WorkspaceFns) Apply(ctx context.Context) (terraform.ApplyResult, error) { 69 return c.ApplyFn(ctx) 70 } 71 72 func (c WorkspaceFns) DestroyAsync(callback terraform.CallbackFn) error { 73 return c.DestroyAsyncFn(callback) 74 } 75 76 func (c WorkspaceFns) Destroy(ctx context.Context) error { 77 return c.DestroyFn(ctx) 78 } 79 80 func (c WorkspaceFns) Refresh(ctx context.Context) (terraform.RefreshResult, error) { 81 return c.RefreshFn(ctx) 82 } 83 84 func (c WorkspaceFns) Plan(ctx context.Context) (terraform.PlanResult, error) { 85 return c.PlanFn(ctx) 86 } 87 88 func (c WorkspaceFns) Import(ctx context.Context, tr resource.Terraformed) (terraform.ImportResult, error) { 89 return c.ImportFn(ctx, tr) 90 } 91 92 type StoreFns struct { 93 WorkspaceFn func(ctx context.Context, c resource.SecretClient, tr resource.Terraformed, ts terraform.Setup, cfg *config.Resource) (*terraform.Workspace, error) 94 } 95 96 func (s StoreFns) Workspace(ctx context.Context, c resource.SecretClient, tr resource.Terraformed, ts terraform.Setup, cfg *config.Resource) (*terraform.Workspace, error) { 97 return s.WorkspaceFn(ctx, c, tr, ts, cfg) 98 } 99 100 type CallbackFns struct { 101 CreateFn func(string) terraform.CallbackFn 102 UpdateFn func(string) terraform.CallbackFn 103 DestroyFn func(string) terraform.CallbackFn 104 } 105 106 func (c CallbackFns) Create(name string) terraform.CallbackFn { 107 return c.CreateFn(name) 108 } 109 110 func (c CallbackFns) Update(name string) terraform.CallbackFn { 111 return c.UpdateFn(name) 112 } 113 114 func (c CallbackFns) Destroy(name string) terraform.CallbackFn { 115 return c.DestroyFn(name) 116 } 117 118 func TestConnect(t *testing.T) { 119 type args struct { 120 setupFn terraform.SetupFn 121 store Store 122 obj xpresource.Managed 123 } 124 type want struct { 125 err error 126 } 127 cases := map[string]struct { 128 reason string 129 args 130 want 131 }{ 132 "WrongType": { 133 args: args{ 134 obj: &xpfake.Managed{}, 135 }, 136 want: want{ 137 err: errors.New(errUnexpectedObject), 138 }, 139 }, 140 "SetupFailed": { 141 reason: "Terraform setup should succeed", 142 args: args{ 143 obj: &fake.Terraformed{}, 144 setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) { 145 return terraform.Setup{}, errBoom 146 }, 147 }, 148 want: want{ 149 err: errors.Wrap(errBoom, errGetTerraformSetup), 150 }, 151 }, 152 "WorkspaceFailed": { 153 reason: "We must get workspace successfully", 154 args: args{ 155 obj: &fake.Terraformed{}, 156 setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) { 157 return terraform.Setup{}, nil 158 }, 159 store: StoreFns{ 160 WorkspaceFn: func(_ context.Context, _ resource.SecretClient, _ resource.Terraformed, _ terraform.Setup, _ *config.Resource) (*terraform.Workspace, error) { 161 return nil, errBoom 162 }, 163 }, 164 }, 165 want: want{ 166 err: errors.Wrap(errBoom, errGetWorkspace), 167 }, 168 }, 169 "Success": { 170 args: args{ 171 obj: &fake.Terraformed{}, 172 setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) { 173 return terraform.Setup{}, nil 174 }, 175 store: StoreFns{ 176 WorkspaceFn: func(_ context.Context, _ resource.SecretClient, _ resource.Terraformed, _ terraform.Setup, _ *config.Resource) (*terraform.Workspace, error) { 177 return terraform.NewWorkspace(testPath), nil 178 }, 179 }, 180 }, 181 }, 182 } 183 for name, tc := range cases { 184 t.Run(name, func(t *testing.T) { 185 c := NewConnector(nil, tc.args.store, tc.args.setupFn, &config.Resource{}) 186 _, err := c.Connect(context.TODO(), tc.args.obj) 187 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 188 t.Errorf("\n%s\nConnect(...): -want error, +got error:\n%s", tc.reason, diff) 189 } 190 }) 191 } 192 } 193 194 func TestObserve(t *testing.T) { 195 type args struct { 196 w Workspace 197 obj xpresource.Managed 198 client client.Client 199 } 200 type want struct { 201 obs managed.ExternalObservation 202 condition *xpv1.Condition 203 err error 204 } 205 cases := map[string]struct { 206 reason string 207 args 208 want 209 }{ 210 "WrongType": { 211 args: args{ 212 obj: &xpfake.Managed{}, 213 }, 214 want: want{ 215 err: errors.New(errUnexpectedObject), 216 }, 217 }, 218 "RefreshFailed": { 219 reason: "It should return error if we cannot refresh", 220 args: args{ 221 obj: &fake.Terraformed{ 222 Managed: xpfake.Managed{ 223 Manageable: xpfake.Manageable{ 224 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, 225 }, 226 }, 227 }, 228 w: WorkspaceFns{ 229 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 230 return terraform.RefreshResult{}, errBoom 231 }, 232 }, 233 }, 234 want: want{ 235 err: errors.Wrap(errBoom, errRefresh), 236 }, 237 }, 238 "RefreshNotFound": { 239 reason: "It should not report error in case resource is not found", 240 args: args{ 241 obj: &fake.Terraformed{ 242 Managed: xpfake.Managed{ 243 Manageable: xpfake.Manageable{ 244 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, 245 }, 246 }, 247 }, 248 w: WorkspaceFns{ 249 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 250 return terraform.RefreshResult{Exists: false}, nil 251 }, 252 }, 253 }, 254 }, 255 "RefreshInProgress": { 256 reason: "It should report exists and up-to-date if an operation is ongoing", 257 args: args{ 258 obj: &fake.Terraformed{ 259 Managed: xpfake.Managed{ 260 Manageable: xpfake.Manageable{ 261 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, 262 }, 263 }, 264 }, 265 w: WorkspaceFns{ 266 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 267 return terraform.RefreshResult{ 268 ASyncInProgress: true, 269 }, nil 270 }, 271 }, 272 }, 273 want: want{ 274 obs: managed.ExternalObservation{ 275 ResourceExists: true, 276 ResourceUpToDate: true, 277 }, 278 }, 279 }, 280 "TransitionToReady": { 281 reason: "We should mark the resource as ready if the refresh succeeds and there is no ongoing operation", 282 args: args{ 283 obj: &fake.Terraformed{ 284 Managed: xpfake.Managed{ 285 ConditionedStatus: xpv1.ConditionedStatus{ 286 // empty 287 }, 288 ObjectMeta: metav1.ObjectMeta{ 289 Annotations: exampleCriticalAnnotations, 290 }, 291 Manageable: xpfake.Manageable{ 292 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, 293 }, 294 }, 295 }, 296 w: WorkspaceFns{ 297 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 298 return terraform.RefreshResult{ 299 Exists: true, 300 State: exampleState, 301 }, nil 302 }, 303 }, 304 }, 305 want: want{ 306 obs: managed.ExternalObservation{ 307 ResourceExists: true, 308 ResourceUpToDate: true, 309 ConnectionDetails: nil, 310 ResourceLateInitialized: false, 311 }, 312 condition: available(), 313 }, 314 }, 315 "PlanFailed": { 316 reason: "Failure of plan should be reported", 317 args: args{ 318 obj: &fake.Terraformed{ 319 Managed: xpfake.Managed{ 320 ObjectMeta: metav1.ObjectMeta{ 321 Annotations: exampleCriticalAnnotations, 322 }, 323 ConditionedStatus: xpv1.ConditionedStatus{ 324 Conditions: []xpv1.Condition{xpv1.Available()}, 325 }, 326 Manageable: xpfake.Manageable{ 327 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, 328 }, 329 }, 330 }, 331 w: WorkspaceFns{ 332 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 333 return terraform.RefreshResult{ 334 Exists: true, 335 State: exampleState, 336 }, nil 337 }, 338 PlanFn: func(_ context.Context) (terraform.PlanResult, error) { 339 return terraform.PlanResult{}, errBoom 340 }, 341 }, 342 }, 343 want: want{ 344 err: errors.Wrap(errBoom, errPlan), 345 }, 346 }, 347 "AnnotationsUpdated": { 348 reason: "We should update annotations if they are not up-to-date as a priority", 349 args: args{ 350 obj: &fake.Terraformed{ 351 Managed: xpfake.Managed{ 352 ConditionedStatus: xpv1.ConditionedStatus{ 353 Conditions: []xpv1.Condition{xpv1.Available()}, 354 }, 355 Manageable: xpfake.Manageable{ 356 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, 357 }, 358 }, 359 }, 360 w: WorkspaceFns{ 361 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 362 return terraform.RefreshResult{ 363 Exists: true, 364 State: exampleState, 365 }, nil 366 }, 367 }, 368 }, 369 want: want{ 370 obs: managed.ExternalObservation{ 371 ResourceExists: true, 372 ResourceUpToDate: true, 373 ResourceLateInitialized: true, 374 }, 375 }, 376 }, 377 "ObserveOnlyAsyncInProgress": { 378 reason: "We should report exists and up-to-date if an operation is ongoing and the policy is observe-only", 379 args: args{ 380 obj: &fake.Terraformed{ 381 Managed: xpfake.Managed{ 382 Manageable: xpfake.Manageable{ 383 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve}, 384 }, 385 ConditionedStatus: xpv1.ConditionedStatus{ 386 Conditions: []xpv1.Condition{xpv1.Available()}, 387 }, 388 }, 389 }, 390 w: WorkspaceFns{ 391 ImportFn: func(ctx context.Context, tr resource.Terraformed) (terraform.ImportResult, error) { 392 return terraform.ImportResult{ 393 ASyncInProgress: true, 394 }, nil 395 }, 396 }, 397 }, 398 want: want{ 399 obs: managed.ExternalObservation{ 400 ResourceExists: true, 401 ResourceUpToDate: true, 402 }, 403 }, 404 }, 405 "ObserveOnlyImportFails": { 406 reason: "We should report an error if the import fails and the policy is observe-only", 407 args: args{ 408 obj: &fake.Terraformed{ 409 Managed: xpfake.Managed{ 410 Manageable: xpfake.Manageable{ 411 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve}, 412 }, 413 ConditionedStatus: xpv1.ConditionedStatus{ 414 Conditions: []xpv1.Condition{xpv1.Available()}, 415 }, 416 }, 417 }, 418 w: WorkspaceFns{ 419 ImportFn: func(ctx context.Context, tr resource.Terraformed) (terraform.ImportResult, error) { 420 return terraform.ImportResult{}, errBoom 421 }, 422 }, 423 }, 424 want: want{ 425 err: errors.Wrap(errBoom, errImport), 426 }, 427 }, 428 "ObserveOnlyDoesNotExist": { 429 reason: "We should report if the resource does not exist and the policy is observe-only", 430 args: args{ 431 obj: &fake.Terraformed{ 432 Managed: xpfake.Managed{ 433 Manageable: xpfake.Manageable{ 434 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve}, 435 }, 436 ConditionedStatus: xpv1.ConditionedStatus{ 437 Conditions: []xpv1.Condition{xpv1.Available()}, 438 }, 439 }, 440 }, 441 w: WorkspaceFns{ 442 ImportFn: func(ctx context.Context, tr resource.Terraformed) (terraform.ImportResult, error) { 443 return terraform.ImportResult{ 444 Exists: false, 445 }, nil 446 }, 447 }, 448 }, 449 want: want{ 450 obs: managed.ExternalObservation{ 451 ResourceExists: false, 452 }, 453 }, 454 }, 455 "ObserveOnlySuccess": { 456 args: args{ 457 obj: &fake.Terraformed{ 458 Managed: xpfake.Managed{ 459 Manageable: xpfake.Manageable{ 460 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve}, 461 }, 462 ConditionedStatus: xpv1.ConditionedStatus{ 463 // empty 464 }, 465 ObjectMeta: metav1.ObjectMeta{ 466 Annotations: exampleCriticalAnnotations, 467 }, 468 }, 469 }, 470 w: WorkspaceFns{ 471 ImportFn: func(ctx context.Context, tr resource.Terraformed) (terraform.ImportResult, error) { 472 return terraform.ImportResult{ 473 Exists: true, 474 State: exampleState, 475 }, nil 476 }, 477 PlanFn: func(_ context.Context) (terraform.PlanResult, error) { 478 return terraform.PlanResult{UpToDate: true}, nil 479 }, 480 }, 481 }, 482 want: want{ 483 obs: managed.ExternalObservation{ 484 ResourceExists: true, 485 ResourceUpToDate: true, 486 }, 487 condition: available(), 488 }, 489 }, 490 "TransitionToReadyManagementPolicyDefault": { 491 reason: "We should mark the resource as ready if the refresh succeeds and there is no ongoing operation", 492 args: args{ 493 obj: &fake.Terraformed{ 494 Managed: xpfake.Managed{ 495 Manageable: xpfake.Manageable{ 496 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll}, 497 }, 498 ConditionedStatus: xpv1.ConditionedStatus{ 499 // empty 500 }, 501 ObjectMeta: metav1.ObjectMeta{ 502 Annotations: exampleCriticalAnnotations, 503 }, 504 }, 505 }, 506 w: WorkspaceFns{ 507 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 508 return terraform.RefreshResult{ 509 Exists: true, 510 State: exampleState, 511 }, nil 512 }, 513 }, 514 }, 515 want: want{ 516 obs: managed.ExternalObservation{ 517 ResourceExists: true, 518 ResourceUpToDate: true, 519 ConnectionDetails: nil, 520 ResourceLateInitialized: false, 521 }, 522 condition: available(), 523 }, 524 }, 525 "AnnotationsUpdatedManuallyManagementPolicyNoLateInit": { 526 reason: "We should update annotations manually if they are not up-to-date and the policy is not late-init", 527 args: args{ 528 client: &test.MockClient{ 529 MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 530 if diff := cmp.Diff(exampleCriticalAnnotations, obj.GetAnnotations()); diff != "" { 531 reason := "Critical annotations should be updated" 532 t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff) 533 } 534 return nil 535 }, 536 }, 537 obj: &fake.Terraformed{ 538 Managed: xpfake.Managed{ 539 ConditionedStatus: xpv1.ConditionedStatus{ 540 // empty 541 }, 542 Manageable: xpfake.Manageable{ 543 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionCreate}, 544 }, 545 }, 546 }, 547 w: WorkspaceFns{ 548 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 549 return terraform.RefreshResult{ 550 Exists: true, 551 State: exampleState, 552 }, nil 553 }, 554 }, 555 }, 556 want: want{ 557 obs: managed.ExternalObservation{ 558 ResourceExists: true, 559 ResourceUpToDate: true, 560 }, 561 condition: available(), 562 }, 563 }, 564 "AnnotationsUpdatedManuallyManagementPolicyNoLateInitError": { 565 reason: "Should handle the error of updating annotations manually if they are not up-to-date and the policy is not late-init", 566 args: args{ 567 client: &test.MockClient{ 568 MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { 569 return errBoom 570 }, 571 }, 572 obj: &fake.Terraformed{ 573 Managed: xpfake.Managed{ 574 Manageable: xpfake.Manageable{ 575 Policy: xpv1.ManagementPolicies{xpv1.ManagementActionCreate}, 576 }, 577 }, 578 }, 579 w: WorkspaceFns{ 580 RefreshFn: func(_ context.Context) (terraform.RefreshResult, error) { 581 return terraform.RefreshResult{ 582 Exists: true, 583 State: exampleState, 584 }, nil 585 }, 586 }, 587 }, 588 want: want{ 589 err: errors.Wrap(errBoom, errUpdateAnnotations), 590 }, 591 }, 592 } 593 for name, tc := range cases { 594 t.Run(name, func(t *testing.T) { 595 e := &external{workspace: tc.w, config: config.DefaultResource("upjet_resource", nil, nil, nil), kube: tc.args.client, logger: logging.NewNopLogger()} 596 observation, err := e.Observe(context.TODO(), tc.args.obj) 597 if diff := cmp.Diff(tc.want.obs, observation); diff != "" { 598 t.Errorf("\n%s\nObserve(...): -want observation, +got observation:\n%s", tc.reason, diff) 599 } 600 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 601 t.Errorf("\n%s\nObserve(...): -want error, +got error:\n%s", tc.reason, diff) 602 } 603 if tc.want.condition != nil { 604 if diff := cmp.Diff(*tc.want.condition, tc.args.obj.GetCondition(tc.want.condition.Type), cmpopts.IgnoreTypes(metav1.Time{})); diff != "" { 605 t.Errorf("\n%s\nObserve(...): -want condition, +got condition:\n%s", tc.reason, diff) 606 } 607 } 608 }) 609 } 610 } 611 612 func available() *xpv1.Condition { 613 c := xpv1.Available() 614 return &c 615 } 616 617 func TestCreate(t *testing.T) { 618 type args struct { 619 w Workspace 620 c CallbackProvider 621 cfg *config.Resource 622 obj xpresource.Managed 623 } 624 type want struct { 625 err error 626 } 627 cases := map[string]struct { 628 reason string 629 args 630 want 631 }{ 632 "WrongType": { 633 args: args{ 634 cfg: &config.Resource{}, 635 obj: &xpfake.Managed{}, 636 }, 637 want: want{ 638 err: errors.New(errUnexpectedObject), 639 }, 640 }, 641 "AsyncFailed": { 642 reason: "It should return error if it cannot trigger the async apply", 643 args: args{ 644 cfg: &config.Resource{ 645 UseAsync: true, 646 }, 647 c: CallbackFns{ 648 CreateFn: func(s string) terraform.CallbackFn { 649 return nil 650 }, 651 }, 652 obj: &fake.Terraformed{}, 653 w: WorkspaceFns{ 654 ApplyAsyncFn: func(_ terraform.CallbackFn) error { 655 return errBoom 656 }, 657 }, 658 }, 659 want: want{ 660 err: errors.Wrap(errBoom, errStartAsyncApply), 661 }, 662 }, 663 "SyncApplyFailed": { 664 reason: "It should return error if it cannot apply in sync mode", 665 args: args{ 666 cfg: &config.Resource{}, 667 obj: &fake.Terraformed{}, 668 w: WorkspaceFns{ 669 ApplyFn: func(_ context.Context) (terraform.ApplyResult, error) { 670 return terraform.ApplyResult{}, errBoom 671 }, 672 }, 673 }, 674 want: want{ 675 err: errors.Wrap(errBoom, errApply), 676 }, 677 }, 678 } 679 for name, tc := range cases { 680 t.Run(name, func(t *testing.T) { 681 e := &external{workspace: tc.w, callback: tc.c, config: tc.cfg} 682 _, err := e.Create(context.TODO(), tc.args.obj) 683 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 684 t.Errorf("\n%s\nCreate(...): -want error, +got error:\n%s", tc.reason, diff) 685 } 686 }) 687 } 688 } 689 690 func TestUpdate(t *testing.T) { 691 type args struct { 692 w Workspace 693 cfg *config.Resource 694 c CallbackProvider 695 obj xpresource.Managed 696 } 697 type want struct { 698 err error 699 } 700 cases := map[string]struct { 701 reason string 702 args 703 want 704 }{ 705 "WrongType": { 706 args: args{ 707 cfg: &config.Resource{}, 708 obj: &xpfake.Managed{}, 709 }, 710 want: want{ 711 err: errors.New(errUnexpectedObject), 712 }, 713 }, 714 "AsyncUpdateFailed": { 715 reason: "It should return error if it cannot trigger the async apply", 716 args: args{ 717 cfg: &config.Resource{ 718 UseAsync: true, 719 }, 720 c: CallbackFns{ 721 UpdateFn: func(s string) terraform.CallbackFn { 722 return nil 723 }, 724 }, 725 obj: &fake.Terraformed{}, 726 w: WorkspaceFns{ 727 ApplyAsyncFn: func(_ terraform.CallbackFn) error { 728 return errBoom 729 }, 730 }, 731 }, 732 want: want{ 733 err: errors.Wrap(errBoom, errStartAsyncApply), 734 }, 735 }, 736 "SyncUpdateFailed": { 737 reason: "It should return error if it cannot apply in sync mode", 738 args: args{ 739 cfg: &config.Resource{}, 740 obj: &fake.Terraformed{}, 741 w: WorkspaceFns{ 742 ApplyFn: func(_ context.Context) (terraform.ApplyResult, error) { 743 return terraform.ApplyResult{}, errBoom 744 }, 745 }, 746 }, 747 want: want{ 748 err: errors.Wrap(errBoom, errApply), 749 }, 750 }, 751 } 752 for name, tc := range cases { 753 t.Run(name, func(t *testing.T) { 754 e := &external{workspace: tc.w, callback: tc.c, config: tc.cfg} 755 _, err := e.Update(context.TODO(), tc.args.obj) 756 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 757 t.Errorf("\n%s\nCreate(...): -want error, +got error:\n%s", tc.reason, diff) 758 } 759 }) 760 } 761 } 762 763 func TestDelete(t *testing.T) { 764 type args struct { 765 w Workspace 766 cfg *config.Resource 767 c CallbackProvider 768 obj xpresource.Managed 769 } 770 type want struct { 771 err error 772 } 773 cases := map[string]struct { 774 reason string 775 args 776 want 777 }{ 778 "AsyncFailed": { 779 reason: "It should return error if it cannot trigger the async destroy", 780 args: args{ 781 cfg: &config.Resource{ 782 UseAsync: true, 783 }, 784 c: CallbackFns{ 785 DestroyFn: func(_ string) terraform.CallbackFn { 786 return nil 787 }, 788 }, 789 obj: &fake.Terraformed{}, 790 w: WorkspaceFns{ 791 DestroyAsyncFn: func(_ terraform.CallbackFn) error { 792 return errBoom 793 }, 794 }, 795 }, 796 want: want{ 797 err: errors.Wrap(errBoom, errStartAsyncDestroy), 798 }, 799 }, 800 "SyncDestroyFailed": { 801 reason: "It should return error if it cannot destroy in sync mode", 802 args: args{ 803 obj: &fake.Terraformed{}, 804 cfg: &config.Resource{}, 805 w: WorkspaceFns{ 806 DestroyFn: func(_ context.Context) error { 807 return errBoom 808 }, 809 }, 810 }, 811 want: want{ 812 err: errors.Wrap(errBoom, errDestroy), 813 }, 814 }, 815 } 816 for name, tc := range cases { 817 t.Run(name, func(t *testing.T) { 818 e := &external{workspace: tc.w, callback: tc.c, config: tc.cfg} 819 err := e.Delete(context.TODO(), tc.args.obj) 820 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 821 t.Errorf("\n%s\nCreate(...): -want error, +got error:\n%s", tc.reason, diff) 822 } 823 }) 824 } 825 }