github.com/crossplane/upjet@v1.3.0/pkg/controller/external_async_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 11 "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" 12 xpresource "github.com/crossplane/crossplane-runtime/pkg/resource" 13 "github.com/crossplane/crossplane-runtime/pkg/test" 14 "github.com/google/go-cmp/cmp" 15 "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 16 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 17 tf "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 18 "sigs.k8s.io/controller-runtime/pkg/client" 19 20 "github.com/crossplane/upjet/pkg/config" 21 "github.com/crossplane/upjet/pkg/resource/fake" 22 "github.com/crossplane/upjet/pkg/terraform" 23 ) 24 25 var ( 26 cfgAsync = &config.Resource{ 27 TerraformResource: &schema.Resource{ 28 Timeouts: &schema.ResourceTimeout{ 29 Create: &timeout, 30 Read: &timeout, 31 Update: &timeout, 32 Delete: &timeout, 33 }, 34 Schema: map[string]*schema.Schema{ 35 "name": { 36 Type: schema.TypeString, 37 Required: true, 38 }, 39 "id": { 40 Type: schema.TypeString, 41 Computed: true, 42 Required: false, 43 }, 44 "map": { 45 Type: schema.TypeMap, 46 Elem: &schema.Schema{ 47 Type: schema.TypeString, 48 }, 49 }, 50 "list": { 51 Type: schema.TypeList, 52 Elem: &schema.Schema{ 53 Type: schema.TypeString, 54 }, 55 }, 56 }, 57 }, 58 ExternalName: config.IdentifierFromProvider, 59 Sensitive: config.Sensitive{AdditionalConnectionDetailsFn: func(attr map[string]any) (map[string][]byte, error) { 60 return nil, nil 61 }}, 62 } 63 objAsync = &fake.Terraformed{ 64 Parameterizable: fake.Parameterizable{ 65 Parameters: map[string]any{ 66 "name": "example", 67 "map": map[string]any{ 68 "key": "value", 69 }, 70 "list": []any{"elem1", "elem2"}, 71 }, 72 }, 73 Observable: fake.Observable{ 74 Observation: map[string]any{}, 75 }, 76 } 77 ) 78 79 func prepareTerraformPluginSDKAsyncExternal(r Resource, cfg *config.Resource, fns CallbackFns) *terraformPluginSDKAsyncExternal { 80 schemaBlock := cfg.TerraformResource.CoreConfigSchema() 81 rawConfig, err := schema.JSONMapToStateValue(map[string]any{"name": "example"}, schemaBlock) 82 if err != nil { 83 panic(err) 84 } 85 return &terraformPluginSDKAsyncExternal{ 86 terraformPluginSDKExternal: &terraformPluginSDKExternal{ 87 ts: terraform.Setup{}, 88 resourceSchema: r, 89 config: cfg, 90 params: map[string]any{ 91 "name": "example", 92 }, 93 rawConfig: rawConfig, 94 logger: logTest, 95 opTracker: NewAsyncTracker(), 96 }, 97 callback: fns, 98 } 99 } 100 101 func TestAsyncTerraformPluginSDKConnect(t *testing.T) { 102 type args struct { 103 setupFn terraform.SetupFn 104 cfg *config.Resource 105 ots *OperationTrackerStore 106 obj xpresource.Managed 107 } 108 type want struct { 109 err error 110 } 111 cases := map[string]struct { 112 args 113 want 114 }{ 115 "Successful": { 116 args: args{ 117 setupFn: func(_ context.Context, _ client.Client, _ xpresource.Managed) (terraform.Setup, error) { 118 return terraform.Setup{}, nil 119 }, 120 cfg: cfgAsync, 121 obj: objAsync, 122 ots: ots, 123 }, 124 }, 125 } 126 for name, tc := range cases { 127 t.Run(name, func(t *testing.T) { 128 c := NewTerraformPluginSDKAsyncConnector(nil, tc.args.ots, tc.args.setupFn, tc.args.cfg, WithTerraformPluginSDKAsyncLogger(logTest)) 129 _, err := c.Connect(context.TODO(), tc.args.obj) 130 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 131 t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) 132 } 133 }) 134 } 135 } 136 137 func TestAsyncTerraformPluginSDKObserve(t *testing.T) { 138 type args struct { 139 r Resource 140 cfg *config.Resource 141 obj xpresource.Managed 142 } 143 type want struct { 144 obs managed.ExternalObservation 145 err error 146 } 147 cases := map[string]struct { 148 args 149 want 150 }{ 151 "NotExists": { 152 args: args{ 153 r: mockResource{ 154 RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { 155 return nil, nil 156 }, 157 }, 158 cfg: cfgAsync, 159 obj: objAsync, 160 }, 161 want: want{ 162 obs: managed.ExternalObservation{ 163 ResourceExists: false, 164 ResourceUpToDate: false, 165 ResourceLateInitialized: false, 166 ConnectionDetails: nil, 167 Diff: "", 168 }, 169 }, 170 }, 171 "UpToDate": { 172 args: args{ 173 r: mockResource{ 174 RefreshWithoutUpgradeFn: func(ctx context.Context, s *tf.InstanceState, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { 175 return &tf.InstanceState{ID: "example-id", Attributes: map[string]string{"name": "example"}}, nil 176 }, 177 }, 178 cfg: cfgAsync, 179 obj: objAsync, 180 }, 181 want: want{ 182 obs: managed.ExternalObservation{ 183 ResourceExists: true, 184 ResourceUpToDate: true, 185 ResourceLateInitialized: true, 186 ConnectionDetails: nil, 187 Diff: "", 188 }, 189 }, 190 }, 191 } 192 for name, tc := range cases { 193 t.Run(name, func(t *testing.T) { 194 terraformPluginSDKAsyncExternal := prepareTerraformPluginSDKAsyncExternal(tc.args.r, tc.args.cfg, CallbackFns{}) 195 observation, err := terraformPluginSDKAsyncExternal.Observe(context.TODO(), tc.args.obj) 196 if diff := cmp.Diff(tc.want.obs, observation); diff != "" { 197 t.Errorf("\n%s\nObserve(...): -want observation, +got observation:\n", diff) 198 } 199 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 200 t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) 201 } 202 }) 203 } 204 } 205 206 func TestAsyncTerraformPluginSDKCreate(t *testing.T) { 207 type args struct { 208 r Resource 209 cfg *config.Resource 210 obj xpresource.Managed 211 fns CallbackFns 212 } 213 type want struct { 214 err error 215 } 216 cases := map[string]struct { 217 args 218 want 219 }{ 220 "Successful": { 221 args: args{ 222 r: mockResource{ 223 ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { 224 return &tf.InstanceState{ID: "example-id"}, nil 225 }, 226 }, 227 cfg: cfgAsync, 228 obj: objAsync, 229 fns: CallbackFns{ 230 CreateFn: func(s string) terraform.CallbackFn { 231 return func(err error, ctx context.Context) error { 232 return nil 233 } 234 }, 235 }, 236 }, 237 }, 238 } 239 for name, tc := range cases { 240 t.Run(name, func(t *testing.T) { 241 terraformPluginSDKAsyncExternal := prepareTerraformPluginSDKAsyncExternal(tc.args.r, tc.args.cfg, tc.args.fns) 242 _, err := terraformPluginSDKAsyncExternal.Create(context.TODO(), tc.args.obj) 243 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 244 t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) 245 } 246 }) 247 } 248 } 249 250 func TestAsyncTerraformPluginSDKUpdate(t *testing.T) { 251 type args struct { 252 r Resource 253 cfg *config.Resource 254 obj xpresource.Managed 255 fns CallbackFns 256 } 257 type want struct { 258 err error 259 } 260 cases := map[string]struct { 261 args 262 want 263 }{ 264 "Successful": { 265 args: args{ 266 r: mockResource{ 267 ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { 268 return &tf.InstanceState{ID: "example-id"}, nil 269 }, 270 }, 271 cfg: cfgAsync, 272 obj: objAsync, 273 fns: CallbackFns{ 274 UpdateFn: func(s string) terraform.CallbackFn { 275 return func(err error, ctx context.Context) error { 276 return nil 277 } 278 }, 279 }, 280 }, 281 }, 282 } 283 for name, tc := range cases { 284 t.Run(name, func(t *testing.T) { 285 terraformPluginSDKAsyncExternal := prepareTerraformPluginSDKAsyncExternal(tc.args.r, tc.args.cfg, tc.args.fns) 286 _, err := terraformPluginSDKAsyncExternal.Update(context.TODO(), tc.args.obj) 287 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 288 t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) 289 } 290 }) 291 } 292 } 293 294 func TestAsyncTerraformPluginSDKDelete(t *testing.T) { 295 type args struct { 296 r Resource 297 cfg *config.Resource 298 obj xpresource.Managed 299 fns CallbackFns 300 } 301 type want struct { 302 err error 303 } 304 cases := map[string]struct { 305 args 306 want 307 }{ 308 "Successful": { 309 args: args{ 310 r: mockResource{ 311 ApplyFn: func(ctx context.Context, s *tf.InstanceState, d *tf.InstanceDiff, meta interface{}) (*tf.InstanceState, diag.Diagnostics) { 312 return &tf.InstanceState{ID: "example-id"}, nil 313 }, 314 }, 315 cfg: cfgAsync, 316 obj: objAsync, 317 fns: CallbackFns{ 318 DestroyFn: func(s string) terraform.CallbackFn { 319 return func(err error, ctx context.Context) error { 320 return nil 321 } 322 }, 323 }, 324 }, 325 }, 326 } 327 for name, tc := range cases { 328 t.Run(name, func(t *testing.T) { 329 terraformPluginSDKAsyncExternal := prepareTerraformPluginSDKAsyncExternal(tc.args.r, tc.args.cfg, tc.args.fns) 330 err := terraformPluginSDKAsyncExternal.Delete(context.TODO(), tc.args.obj) 331 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 332 t.Errorf("\n%s\nConnect(...): -want error, +got error:\n", diff) 333 } 334 }) 335 } 336 }