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 }