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  }