github.com/crossplane/upjet@v1.3.0/pkg/controller/external_tfpluginsdk_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  	"time"
    11  
    12  	"github.com/crossplane/crossplane-runtime/pkg/logging"
    13  	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
    14  	xpresource "github.com/crossplane/crossplane-runtime/pkg/resource"
    15  	"github.com/crossplane/crossplane-runtime/pkg/test"
    16  	"github.com/google/go-cmp/cmp"
    17  	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
    18  	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    19  	tf "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    20  	"github.com/pkg/errors"
    21  	"sigs.k8s.io/controller-runtime/pkg/client"
    22  	"sigs.k8s.io/controller-runtime/pkg/log/zap"
    23  
    24  	"github.com/crossplane/upjet/pkg/config"
    25  	"github.com/crossplane/upjet/pkg/resource/fake"
    26  	"github.com/crossplane/upjet/pkg/terraform"
    27  )
    28  
    29  var (
    30  	zl      = zap.New(zap.UseDevMode(true))
    31  	logTest = logging.NewLogrLogger(zl.WithName("provider-aws"))
    32  	ots     = NewOperationStore(logTest)
    33  	timeout = time.Duration(1200000000000)
    34  	cfg     = &config.Resource{
    35  		TerraformResource: &schema.Resource{
    36  			Timeouts: &schema.ResourceTimeout{
    37  				Create: &timeout,
    38  				Read:   &timeout,
    39  				Update: &timeout,
    40  				Delete: &timeout,
    41  			},
    42  			Schema: map[string]*schema.Schema{
    43  				"name": {
    44  					Type:     schema.TypeString,
    45  					Required: true,
    46  				},
    47  				"id": {
    48  					Type:     schema.TypeString,
    49  					Computed: true,
    50  					Required: false,
    51  				},
    52  				"map": {
    53  					Type: schema.TypeMap,
    54  					Elem: &schema.Schema{
    55  						Type: schema.TypeString,
    56  					},
    57  				},
    58  				"list": {
    59  					Type: schema.TypeList,
    60  					Elem: &schema.Schema{
    61  						Type: schema.TypeString,
    62  					},
    63  				},
    64  			},
    65  		},
    66  		ExternalName: config.IdentifierFromProvider,
    67  		Sensitive: config.Sensitive{AdditionalConnectionDetailsFn: func(attr map[string]any) (map[string][]byte, error) {
    68  			return nil, nil
    69  		}},
    70  	}
    71  	obj = fake.Terraformed{
    72  		Parameterizable: fake.Parameterizable{
    73  			Parameters: map[string]any{
    74  				"name": "example",
    75  				"map": map[string]any{
    76  					"key": "value",
    77  				},
    78  				"list": []any{"elem1", "elem2"},
    79  			},
    80  		},
    81  		Observable: fake.Observable{
    82  			Observation: map[string]any{},
    83  		},
    84  	}
    85  )
    86  
    87  func prepareTerraformPluginSDKExternal(r Resource, cfg *config.Resource) *terraformPluginSDKExternal {
    88  	schemaBlock := cfg.TerraformResource.CoreConfigSchema()
    89  	rawConfig, err := schema.JSONMapToStateValue(map[string]any{"name": "example"}, schemaBlock)
    90  	if err != nil {
    91  		panic(err)
    92  	}
    93  	return &terraformPluginSDKExternal{
    94  		ts:             terraform.Setup{},
    95  		resourceSchema: r,
    96  		config:         cfg,
    97  		params: map[string]any{
    98  			"name": "example",
    99  		},
   100  		rawConfig: rawConfig,
   101  		logger:    logTest,
   102  		opTracker: NewAsyncTracker(),
   103  	}
   104  }
   105  
   106  type mockResource struct {
   107  	ApplyFn                 func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics)
   108  	RefreshWithoutUpgradeFn func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics)
   109  }
   110  
   111  func (m mockResource) Apply(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   112  	return m.ApplyFn(ctx, s, d, meta)
   113  }
   114  
   115  func (m mockResource) RefreshWithoutUpgrade(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   116  	return m.RefreshWithoutUpgradeFn(ctx, s, meta)
   117  }
   118  
   119  func TestTerraformPluginSDKConnect(t *testing.T) {
   120  	type args struct {
   121  		setupFn terraform.SetupFn
   122  		cfg     *config.Resource
   123  		ots     *OperationTrackerStore
   124  		obj     fake.Terraformed
   125  	}
   126  	type want struct {
   127  		err error
   128  	}
   129  	cases := map[string]struct {
   130  		args
   131  		want
   132  	}{
   133  		"Successful": {
   134  			args: args{
   135  				setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) {
   136  					return terraform.Setup{}, nil
   137  				},
   138  				cfg: cfg,
   139  				obj: obj,
   140  				ots: ots,
   141  			},
   142  		},
   143  		"HCL": {
   144  			args: args{
   145  				setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) {
   146  					return terraform.Setup{}, nil
   147  				},
   148  				cfg: cfg,
   149  				obj: fake.Terraformed{
   150  					Parameterizable: fake.Parameterizable{
   151  						Parameters: map[string]any{
   152  							"name": "      ${jsonencode({\n          type = \"object\"\n        })}",
   153  							"map": map[string]any{
   154  								"key": "value",
   155  							},
   156  							"list": []any{"elem1", "elem2"},
   157  						},
   158  					},
   159  					Observable: fake.Observable{
   160  						Observation: map[string]any{},
   161  					},
   162  				},
   163  				ots: ots,
   164  			},
   165  		},
   166  	}
   167  
   168  	for name, tc := range cases {
   169  		t.Run(name, func(t *testing.T) {
   170  			c := NewTerraformPluginSDKConnector(nil, tc.args.setupFn, tc.args.cfg, tc.args.ots, WithTerraformPluginSDKLogger(logTest))
   171  			_, err := c.Connect(context.TODO(), &tc.args.obj)
   172  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   173  				t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff)
   174  			}
   175  		})
   176  	}
   177  }
   178  
   179  func TestTerraformPluginSDKObserve(t *testing.T) {
   180  	type args struct {
   181  		r   Resource
   182  		cfg *config.Resource
   183  		obj fake.Terraformed
   184  	}
   185  	type want struct {
   186  		obs managed.ExternalObservation
   187  		err error
   188  	}
   189  	cases := map[string]struct {
   190  		args
   191  		want
   192  	}{
   193  		"NotExists": {
   194  			args: args{
   195  				r: mockResource{
   196  					RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   197  						return nil, nil
   198  					},
   199  				},
   200  				cfg: cfg,
   201  				obj: obj,
   202  			},
   203  			want: want{
   204  				obs: managed.ExternalObservation{
   205  					ResourceExists:          false,
   206  					ResourceUpToDate:        false,
   207  					ResourceLateInitialized: false,
   208  					ConnectionDetails:       nil,
   209  					Diff:                    "",
   210  				},
   211  			},
   212  		},
   213  		"UpToDate": {
   214  			args: args{
   215  				r: mockResource{
   216  					RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   217  						return &tf.InstanceState{ID: "example-id", Attributes: map[string]string{"name": "example"}}, nil
   218  					},
   219  				},
   220  				cfg: cfg,
   221  				obj: obj,
   222  			},
   223  			want: want{
   224  				obs: managed.ExternalObservation{
   225  					ResourceExists:          true,
   226  					ResourceUpToDate:        true,
   227  					ResourceLateInitialized: true,
   228  					ConnectionDetails:       nil,
   229  					Diff:                    "",
   230  				},
   231  			},
   232  		},
   233  		"InitProvider": {
   234  			args: args{
   235  				r: mockResource{
   236  					RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   237  						return &tf.InstanceState{ID: "example-id", Attributes: map[string]string{"name": "example2"}}, nil
   238  					},
   239  				},
   240  				cfg: cfg,
   241  				obj: fake.Terraformed{
   242  					Parameterizable: fake.Parameterizable{
   243  						Parameters: map[string]any{
   244  							"name": "example",
   245  							"map": map[string]any{
   246  								"key": "value",
   247  							},
   248  							"list": []any{"elem1", "elem2"},
   249  						},
   250  						InitParameters: map[string]any{
   251  							"list": []any{"elem1", "elem2", "elem3"},
   252  						},
   253  					},
   254  					Observable: fake.Observable{
   255  						Observation: map[string]any{},
   256  					},
   257  				},
   258  			},
   259  			want: want{
   260  				obs: managed.ExternalObservation{
   261  					ResourceExists:          true,
   262  					ResourceUpToDate:        false,
   263  					ResourceLateInitialized: true,
   264  					ConnectionDetails:       nil,
   265  					Diff:                    "",
   266  				},
   267  			},
   268  		},
   269  	}
   270  
   271  	for name, tc := range cases {
   272  		t.Run(name, func(t *testing.T) {
   273  			terraformPluginSDKExternal := prepareTerraformPluginSDKExternal(tc.args.r, tc.args.cfg)
   274  			observation, err := terraformPluginSDKExternal.Observe(context.TODO(), &tc.args.obj)
   275  			if diff := cmp.Diff(tc.want.obs, observation); diff != "" {
   276  				t.Errorf("\n%s\nObserve(...): -want observation, +got observation:\n", diff)
   277  			}
   278  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   279  				t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff)
   280  			}
   281  		})
   282  	}
   283  }
   284  
   285  func TestTerraformPluginSDKCreate(t *testing.T) {
   286  	type args struct {
   287  		r   Resource
   288  		cfg *config.Resource
   289  		obj fake.Terraformed
   290  	}
   291  	type want struct {
   292  		err error
   293  	}
   294  	cases := map[string]struct {
   295  		args
   296  		want
   297  	}{
   298  		"Unsuccessful": {
   299  			args: args{
   300  				r: mockResource{
   301  					ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   302  						return nil, nil
   303  					},
   304  				},
   305  				cfg: cfg,
   306  				obj: obj,
   307  			},
   308  			want: want{
   309  				err: errors.New("failed to read the ID of the new resource"),
   310  			},
   311  		},
   312  		"Successful": {
   313  			args: args{
   314  				r: mockResource{
   315  					ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   316  						return &tf.InstanceState{ID: "example-id"}, nil
   317  					},
   318  				},
   319  				cfg: cfg,
   320  				obj: obj,
   321  			},
   322  		},
   323  	}
   324  	for name, tc := range cases {
   325  		t.Run(name, func(t *testing.T) {
   326  			terraformPluginSDKExternal := prepareTerraformPluginSDKExternal(tc.args.r, tc.args.cfg)
   327  			_, err := terraformPluginSDKExternal.Create(context.TODO(), &tc.args.obj)
   328  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   329  				t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff)
   330  			}
   331  		})
   332  	}
   333  }
   334  
   335  func TestTerraformPluginSDKUpdate(t *testing.T) {
   336  	type args struct {
   337  		r   Resource
   338  		cfg *config.Resource
   339  		obj fake.Terraformed
   340  	}
   341  	type want struct {
   342  		err error
   343  	}
   344  	cases := map[string]struct {
   345  		args
   346  		want
   347  	}{
   348  		"Successful": {
   349  			args: args{
   350  				r: mockResource{
   351  					ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   352  						return &tf.InstanceState{ID: "example-id"}, nil
   353  					},
   354  				},
   355  				cfg: cfg,
   356  				obj: obj,
   357  			},
   358  		},
   359  	}
   360  	for name, tc := range cases {
   361  		t.Run(name, func(t *testing.T) {
   362  			terraformPluginSDKExternal := prepareTerraformPluginSDKExternal(tc.args.r, tc.args.cfg)
   363  			_, err := terraformPluginSDKExternal.Update(context.TODO(), &tc.args.obj)
   364  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   365  				t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff)
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  func TestTerraformPluginSDKDelete(t *testing.T) {
   372  	type args struct {
   373  		r   Resource
   374  		cfg *config.Resource
   375  		obj fake.Terraformed
   376  	}
   377  	type want struct {
   378  		err error
   379  	}
   380  	cases := map[string]struct {
   381  		args
   382  		want
   383  	}{
   384  		"Successful": {
   385  			args: args{
   386  				r: mockResource{
   387  					ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) {
   388  						return &tf.InstanceState{ID: "example-id"}, nil
   389  					},
   390  				},
   391  				cfg: cfg,
   392  				obj: obj,
   393  			},
   394  		},
   395  	}
   396  	for name, tc := range cases {
   397  		t.Run(name, func(t *testing.T) {
   398  			terraformPluginSDKExternal := prepareTerraformPluginSDKExternal(tc.args.r, tc.args.cfg)
   399  			err := terraformPluginSDKExternal.Delete(context.TODO(), &tc.args.obj)
   400  			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
   401  				t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff)
   402  			}
   403  		})
   404  	}
   405  }