github.com/crossplane/upjet@v1.3.0/pkg/terraform/workspace_test.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package terraform 6 7 import ( 8 "context" 9 "testing" 10 "time" 11 12 "github.com/crossplane/crossplane-runtime/pkg/test" 13 "github.com/google/go-cmp/cmp" 14 "github.com/pkg/errors" 15 "github.com/spf13/afero" 16 k8sExec "k8s.io/utils/exec" 17 testingexec "k8s.io/utils/exec/testing" 18 19 "github.com/crossplane/upjet/pkg/resource/json" 20 tferrors "github.com/crossplane/upjet/pkg/terraform/errors" 21 ) 22 23 var ( 24 testType = "very-cool-type" 25 applyType = "apply" 26 lineage = "very-cool-lineage" 27 terraformVersion = "1.0.10" 28 version = 1 29 serial = 3 30 directory = "random-dir/" 31 changeSummaryAdd = `{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"0000-00-00T00:00:00.000000+03:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}` 32 changeSummaryUpdate = `{"@level":"info","@message":"Plan: 0 to add, 1 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"0000-00-00T00:00:00.000000+03:00","changes":{"add":0,"change":1,"remove":0,"operation":"plan"},"type":"change_summary"}` 33 changeSummaryNoAction = `{"@level":"info","@message":"Plan: 0 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"0000-00-00T00:00:00.000000+03:00","changes":{"add":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}` 34 filter = `{"@level":"info","@message":"Terraform 1.2.1","@module":"terraform.ui","@timestamp":"2022-08-08T14:42:59.377073+03:00","terraform":"1.2.1","type":"version","ui":"1.0"} 35 {"@level":"error","@message":"Error: error configuring Terraform AWS Provider: error validating provider credentials: error calling sts:GetCallerIdentity: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: *****, api error InvalidClientTokenId: The security token included in the request is invalid.","@module":"terraform.ui","@timestamp":"2022-08-08T14:43:00.808602+03:00","diagnostic":{"severity":"error","summary":"error configuring Terraform AWS Provider: error validating provider credentials: error calling sts:GetCallerIdentity: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: *****, api error InvalidClientTokenId: The security token included in the request is invalid.","detail":"","address":"provider[\"registry.terraform.io/hashicorp/aws\"]","range":{"filename":"main.tf.json","start":{"line":1,"column":173,"byte":172},"end":{"line":1,"column":174,"byte":173}},"snippet":{"context":"provider.aws","code":"{\"provider\":{\"aws\":{\"access_key\":\"*****\",\"region\":\"us-east-1\",\"secret_key\":\"/*****\",\"skip_region_validation\":true,\"token\":\"\"}},\"resource\":{\"aws_iam_user\":{\"sample-user\":{\"lifecycle\":{\"prevent_destroy\":true},\"name\":\"sample-user\",\"tags\":{\"crossplane-kind\":\"user.iam.aws.upbound.io\",\"crossplane-name\":\"sample-user\",\"crossplane-providerconfig\":\"default\"}}}},\"terraform\":{\"required_providers\":{\"aws\":{\"source\":\"hashicorp/aws\",\"version\":\"4.15.1\"}}}}","start_line":1,"highlight_start_offset":172,"highlight_end_offset":173,"values":[]}},"type":"diagnostic"}` 36 37 state = &json.StateV4{ 38 Version: uint64(version), 39 TerraformVersion: terraformVersion, 40 Serial: uint64(serial), 41 Lineage: lineage, 42 RootOutputs: map[string]json.OutputStateV4{}, 43 Resources: []json.ResourceStateV4{}, 44 } 45 46 now = time.Now() 47 48 fs = afero.Afero{ 49 Fs: afero.NewMemMapFs(), 50 } 51 52 tfstate = `{"version": 1,"terraform_version": "1.0.10","serial": 3,"lineage": "very-cool-lineage","outputs": {},"resources": []}` 53 54 filterFn = func(s string) string { 55 return "" 56 } 57 ) 58 59 func newFakeExec(stdOut string, err error) *testingexec.FakeExec { 60 return &testingexec.FakeExec{ 61 CommandScript: []testingexec.FakeCommandAction{ 62 func(_ string, _ ...string) k8sExec.Cmd { 63 return &testingexec.FakeCmd{ 64 CombinedOutputScript: []testingexec.FakeAction{ 65 func() ([]byte, []byte, error) { 66 return []byte(stdOut), nil, err 67 }, 68 }, 69 } 70 }, 71 }, 72 } 73 } 74 75 func TestWorkspaceApply(t *testing.T) { 76 type args struct { 77 w *Workspace 78 } 79 type want struct { 80 r ApplyResult 81 err error 82 } 83 84 cases := map[string]struct { 85 args 86 want 87 }{ 88 "Running": { 89 args: args{ 90 w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}), 91 WithAferoFs(fs), WithFilterFn(filterFn)), 92 }, 93 want: want{ 94 err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()), 95 }, 96 }, 97 "Success": { 98 args: args{ 99 w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithAferoFs(fs), 100 WithFilterFn(filterFn), WithProviderInUse(noopInUse{})), 101 }, 102 want: want{ 103 r: ApplyResult{ 104 State: state, 105 }, 106 }, 107 }, 108 "Failure": { 109 args: args{ 110 w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithAferoFs(fs), 111 WithFilterFn(filterFn), WithProviderInUse(noopInUse{})), 112 }, 113 want: want{ 114 err: tferrors.NewApplyFailed([]byte(errBoom.Error())), 115 }, 116 }, 117 "Filter": { 118 args: args{ 119 w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs), 120 WithFilterFn(filterFn)), 121 }, 122 want: want{ 123 err: tferrors.NewApplyFailed([]byte(filter)), 124 }, 125 }, 126 } 127 128 for name, tc := range cases { 129 t.Run(name, func(t *testing.T) { 130 if err := tc.w.fs.WriteFile(directory+"terraform.tfstate", []byte(tfstate), 0777); err != nil { 131 panic(err) 132 } 133 r, err := tc.w.Apply(context.TODO()) 134 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 135 t.Errorf("\n%s\nApply(...): -want error, +got error:\n%s", name, diff) 136 } 137 if diff := cmp.Diff(tc.want.r, r, test.EquateErrors()); diff != "" { 138 t.Errorf("\n%s\nApply(...): -want error, +got error:\n%s", name, diff) 139 } 140 }) 141 } 142 } 143 144 func TestWorkspaceDestroy(t *testing.T) { 145 type args struct { 146 w *Workspace 147 } 148 type want struct { 149 err error 150 } 151 152 cases := map[string]struct { 153 args 154 want 155 }{ 156 "Running": { 157 args: args{ 158 w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}), 159 WithFilterFn(filterFn)), 160 }, 161 want: want{ 162 err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()), 163 }, 164 }, 165 "Success": { 166 args: args{ 167 w: NewWorkspace( 168 directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)), 169 }, 170 want: want{}, 171 }, 172 "Failure": { 173 args: args{ 174 w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithFilterFn(filterFn)), 175 }, 176 want: want{ 177 err: tferrors.NewDestroyFailed([]byte(errBoom.Error())), 178 }, 179 }, 180 "Filter": { 181 args: args{ 182 w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs), 183 WithFilterFn(filterFn)), 184 }, 185 want: want{ 186 err: tferrors.NewDestroyFailed([]byte(filter)), 187 }, 188 }, 189 } 190 191 for name, tc := range cases { 192 t.Run(name, func(t *testing.T) { 193 err := tc.w.Destroy(context.TODO()) 194 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 195 t.Errorf("\n%s\nDestroy(...): -want error, +got error:\n%s", name, diff) 196 } 197 }) 198 } 199 } 200 201 func TestWorkspaceRefresh(t *testing.T) { 202 type args struct { 203 w *Workspace 204 } 205 type want struct { 206 r RefreshResult 207 err error 208 } 209 210 cases := map[string]struct { 211 args 212 want 213 }{ 214 "Running": { 215 args: args{ 216 w: NewWorkspace(directory, WithLastOperation(&Operation{Type: applyType, startTime: &now, endTime: nil}), 217 WithAferoFs(fs), WithFilterFn(filterFn)), 218 }, 219 want: want{ 220 r: RefreshResult{ 221 ASyncInProgress: true, 222 }, 223 }, 224 }, 225 "Success": { 226 args: args{ 227 w: NewWorkspace( 228 directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithAferoFs(fs), 229 WithFilterFn(filterFn)), 230 }, 231 want: want{ 232 r: RefreshResult{ 233 State: state, 234 }, 235 }, 236 }, 237 "Failure": { 238 args: args{ 239 w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithAferoFs(fs), 240 WithFilterFn(filterFn)), 241 }, 242 want: want{ 243 err: tferrors.NewRefreshFailed([]byte(errBoom.Error())), 244 }, 245 }, 246 "Filter": { 247 args: args{ 248 w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs), 249 WithFilterFn(filterFn)), 250 }, 251 want: want{ 252 err: tferrors.NewRefreshFailed([]byte(filter)), 253 }, 254 }, 255 } 256 257 for name, tc := range cases { 258 t.Run(name, func(t *testing.T) { 259 if err := tc.w.fs.WriteFile(directory+"terraform.tfstate", []byte(tfstate), 0777); err != nil { 260 panic(err) 261 } 262 r, err := tc.w.Refresh(context.TODO()) 263 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 264 t.Errorf("\n%s\nRefresh(...): -want error, +got error:\n%s", name, diff) 265 } 266 if diff := cmp.Diff(tc.want.r, r, test.EquateErrors()); diff != "" { 267 t.Errorf("\n%s\nRefresh(...): -want error, +got error:\n%s", name, diff) 268 } 269 }) 270 } 271 } 272 273 func TestWorkspacePlan(t *testing.T) { 274 type args struct { 275 w *Workspace 276 } 277 type want struct { 278 r PlanResult 279 err error 280 } 281 282 cases := map[string]struct { 283 args 284 want 285 }{ 286 "Running": { 287 args: args{ 288 w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil})), 289 }, 290 want: want{ 291 err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()), 292 }, 293 }, 294 "NoChangeSummary": { 295 args: args{ 296 w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)), 297 }, 298 want: want{ 299 err: errors.Errorf("cannot find the change summary line in plan log: "), 300 }, 301 }, 302 "ChangeSummaryAdd": { 303 args: args{ 304 w: NewWorkspace(directory, WithExecutor(newFakeExec(changeSummaryAdd, nil)), WithFilterFn(filterFn)), 305 }, 306 want: want{ 307 r: PlanResult{ 308 Exists: false, 309 UpToDate: true, 310 }, 311 }, 312 }, 313 "ChangeSummaryUpdate": { 314 args: args{ 315 w: NewWorkspace(directory, WithExecutor(newFakeExec(changeSummaryUpdate, nil)), WithFilterFn(filterFn)), 316 }, 317 want: want{ 318 r: PlanResult{ 319 Exists: true, 320 UpToDate: false, 321 }, 322 }, 323 }, 324 "ChangeSummaryNoAction": { 325 args: args{ 326 w: NewWorkspace(directory, WithExecutor(newFakeExec(changeSummaryNoAction, nil)), WithFilterFn(filterFn)), 327 }, 328 want: want{ 329 r: PlanResult{ 330 Exists: true, 331 UpToDate: true, 332 }, 333 }, 334 }, 335 "Failure": { 336 args: args{ 337 w: NewWorkspace(directory, WithExecutor(newFakeExec(errBoom.Error(), errBoom)), WithFilterFn(filterFn)), 338 }, 339 want: want{ 340 err: tferrors.NewPlanFailed([]byte(errBoom.Error())), 341 }, 342 }, 343 "Filter": { 344 args: args{ 345 w: NewWorkspace(directory, WithExecutor(newFakeExec(filter, errors.New(filter))), WithAferoFs(fs), WithFilterFn(filterFn)), 346 }, 347 want: want{ 348 err: tferrors.NewPlanFailed([]byte(filter)), 349 }, 350 }, 351 } 352 353 for name, tc := range cases { 354 t.Run(name, func(t *testing.T) { 355 r, err := tc.w.Plan(context.TODO()) 356 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 357 t.Errorf("\n%s\nPlan(...): -want error, +got error:\n%s", name, diff) 358 } 359 if diff := cmp.Diff(tc.want.r, r, test.EquateErrors()); diff != "" { 360 t.Errorf("\n%s\nPlan(...): -want error, +got error:\n%s", name, diff) 361 } 362 }) 363 } 364 } 365 366 func TestWorkspaceApplyAsync(t *testing.T) { 367 calls := make(chan bool) 368 369 type args struct { 370 w *Workspace 371 c CallbackFn 372 } 373 type want struct { 374 called bool 375 err error 376 } 377 378 cases := map[string]struct { 379 args 380 want 381 }{ 382 "Running": { 383 args: args{ 384 w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}), 385 WithFilterFn(filterFn)), 386 }, 387 want: want{ 388 err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()), 389 }, 390 }, 391 "Callback": { 392 args: args{ 393 w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)), 394 c: func(err error, ctx context.Context) error { 395 calls <- true 396 return nil 397 }, 398 }, 399 want: want{ 400 called: true, 401 }, 402 }, 403 } 404 405 for name, tc := range cases { 406 t.Run(name, func(t *testing.T) { 407 err := tc.w.ApplyAsync(tc.c) 408 if t.Name() == "TestWorkspaceApplyAsync/Callback" { 409 called := <-calls 410 411 if diff := cmp.Diff(tc.want.called, called, test.EquateErrors()); diff != "" { 412 t.Errorf("\n%s\nApplyAsync(...): -want error, +got error:\n%s", name, diff) 413 } 414 } 415 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 416 t.Errorf("\n%s\nApplyAsync(...): -want error, +got error:\n%s", name, diff) 417 } 418 }) 419 } 420 } 421 422 func TestWorkspaceDestroyAsync(t *testing.T) { 423 calls := make(chan bool) 424 425 type args struct { 426 w *Workspace 427 c CallbackFn 428 } 429 type want struct { 430 called bool 431 err error 432 } 433 434 cases := map[string]struct { 435 args 436 want 437 }{ 438 "Running": { 439 args: args{ 440 w: NewWorkspace(directory, WithLastOperation(&Operation{Type: testType, startTime: &now, endTime: nil}), 441 WithFilterFn(filterFn)), 442 }, 443 want: want{ 444 err: errors.Errorf("%s operation that started at %s is still running", testType, now.String()), 445 }, 446 }, 447 "Callback": { 448 args: args{ 449 w: NewWorkspace(directory, WithExecutor(&testingexec.FakeExec{DisableScripts: true}), WithFilterFn(filterFn)), 450 c: func(err error, ctx context.Context) error { 451 calls <- true 452 return nil 453 }, 454 }, 455 want: want{ 456 called: true, 457 }, 458 }, 459 } 460 461 for name, tc := range cases { 462 t.Run(name, func(t *testing.T) { 463 err := tc.w.DestroyAsync(tc.c) 464 if t.Name() == "TestWorkspaceDestroyAsync/Callback" { 465 called := <-calls 466 467 if diff := cmp.Diff(tc.want.called, called, test.EquateErrors()); diff != "" { 468 t.Errorf("\n%s\nDestroyAsync(...): -want error, +got error:\n%s", name, diff) 469 } 470 } 471 if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 472 t.Errorf("\n%s\nDestroyAsync(...): -want error, +got error:\n%s", name, diff) 473 } 474 }) 475 } 476 }