github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/cloud/backend_test.go (about) 1 package cloud 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "os" 8 "strings" 9 "testing" 10 11 tfe "github.com/hashicorp/go-tfe" 12 version "github.com/hashicorp/go-version" 13 "github.com/cycloidio/terraform/backend" 14 "github.com/cycloidio/terraform/tfdiags" 15 tfversion "github.com/cycloidio/terraform/version" 16 "github.com/zclconf/go-cty/cty" 17 18 backendLocal "github.com/cycloidio/terraform/backend/local" 19 ) 20 21 func TestCloud(t *testing.T) { 22 var _ backend.Enhanced = New(nil) 23 var _ backend.CLI = New(nil) 24 } 25 26 func TestCloud_backendWithName(t *testing.T) { 27 b, bCleanup := testBackendWithName(t) 28 defer bCleanup() 29 30 workspaces, err := b.Workspaces() 31 if err != nil { 32 t.Fatalf("error: %v", err) 33 } 34 35 if len(workspaces) != 1 || workspaces[0] != testBackendSingleWorkspaceName { 36 t.Fatalf("should only have a single configured workspace matching the configured 'name' strategy, but got: %#v", workspaces) 37 } 38 39 if _, err := b.StateMgr("foo"); err != backend.ErrWorkspacesNotSupported { 40 t.Fatalf("expected fetching a state which is NOT the single configured workspace to have an ErrWorkspacesNotSupported error, but got: %v", err) 41 } 42 43 if err := b.DeleteWorkspace(testBackendSingleWorkspaceName); err != backend.ErrWorkspacesNotSupported { 44 t.Fatalf("expected deleting the single configured workspace name to result in an error, but got: %v", err) 45 } 46 47 if err := b.DeleteWorkspace("foo"); err != backend.ErrWorkspacesNotSupported { 48 t.Fatalf("expected deleting a workspace which is NOT the configured workspace name to result in an error, but got: %v", err) 49 } 50 } 51 52 func TestCloud_backendWithTags(t *testing.T) { 53 b, bCleanup := testBackendWithTags(t) 54 defer bCleanup() 55 56 backend.TestBackendStates(t, b) 57 58 // Test pagination works 59 for i := 0; i < 25; i++ { 60 _, err := b.StateMgr(fmt.Sprintf("foo-%d", i+1)) 61 if err != nil { 62 t.Fatalf("error: %s", err) 63 } 64 } 65 66 workspaces, err := b.Workspaces() 67 if err != nil { 68 t.Fatalf("error: %s", err) 69 } 70 actual := len(workspaces) 71 if actual != 26 { 72 t.Errorf("expected 26 workspaces (over one standard paginated response), got %d", actual) 73 } 74 } 75 76 func TestCloud_PrepareConfig(t *testing.T) { 77 cases := map[string]struct { 78 config cty.Value 79 expectedErr string 80 }{ 81 "null organization": { 82 config: cty.ObjectVal(map[string]cty.Value{ 83 "organization": cty.NullVal(cty.String), 84 "workspaces": cty.ObjectVal(map[string]cty.Value{ 85 "name": cty.StringVal("prod"), 86 "tags": cty.NullVal(cty.Set(cty.String)), 87 }), 88 }), 89 expectedErr: `Invalid organization value: The "organization" attribute value must not be empty.`, 90 }, 91 "null workspace": { 92 config: cty.ObjectVal(map[string]cty.Value{ 93 "organization": cty.StringVal("org"), 94 "workspaces": cty.NullVal(cty.String), 95 }), 96 expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`, 97 }, 98 "workspace: empty tags, name": { 99 config: cty.ObjectVal(map[string]cty.Value{ 100 "organization": cty.StringVal("org"), 101 "workspaces": cty.ObjectVal(map[string]cty.Value{ 102 "name": cty.NullVal(cty.String), 103 "tags": cty.NullVal(cty.Set(cty.String)), 104 }), 105 }), 106 expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`, 107 }, 108 "workspace: name present": { 109 config: cty.ObjectVal(map[string]cty.Value{ 110 "organization": cty.StringVal("org"), 111 "workspaces": cty.ObjectVal(map[string]cty.Value{ 112 "name": cty.StringVal("prod"), 113 "tags": cty.NullVal(cty.Set(cty.String)), 114 }), 115 }), 116 expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`, 117 }, 118 "workspace: name and tags present": { 119 config: cty.ObjectVal(map[string]cty.Value{ 120 "organization": cty.StringVal("org"), 121 "workspaces": cty.ObjectVal(map[string]cty.Value{ 122 "name": cty.StringVal("prod"), 123 "tags": cty.SetVal( 124 []cty.Value{ 125 cty.StringVal("billing"), 126 }, 127 ), 128 }), 129 }), 130 expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`, 131 }, 132 } 133 134 for name, tc := range cases { 135 s := testServer(t) 136 b := New(testDisco(s)) 137 138 // Validate 139 _, valDiags := b.PrepareConfig(tc.config) 140 if valDiags.Err() != nil && tc.expectedErr != "" { 141 actualErr := valDiags.Err().Error() 142 if !strings.Contains(actualErr, tc.expectedErr) { 143 t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) 144 } 145 } 146 } 147 } 148 149 func TestCloud_config(t *testing.T) { 150 cases := map[string]struct { 151 config cty.Value 152 confErr string 153 valErr string 154 }{ 155 "with_a_nonexisting_organization": { 156 config: cty.ObjectVal(map[string]cty.Value{ 157 "hostname": cty.NullVal(cty.String), 158 "organization": cty.StringVal("nonexisting"), 159 "token": cty.NullVal(cty.String), 160 "workspaces": cty.ObjectVal(map[string]cty.Value{ 161 "name": cty.StringVal("prod"), 162 "tags": cty.NullVal(cty.Set(cty.String)), 163 }), 164 }), 165 confErr: "organization \"nonexisting\" at host app.terraform.io not found", 166 }, 167 "with_an_unknown_host": { 168 config: cty.ObjectVal(map[string]cty.Value{ 169 "hostname": cty.StringVal("nonexisting.local"), 170 "organization": cty.StringVal("hashicorp"), 171 "token": cty.NullVal(cty.String), 172 "workspaces": cty.ObjectVal(map[string]cty.Value{ 173 "name": cty.StringVal("prod"), 174 "tags": cty.NullVal(cty.Set(cty.String)), 175 }), 176 }), 177 confErr: "Failed to request discovery document", 178 }, 179 // localhost advertises TFE services, but has no token in the credentials 180 "without_a_token": { 181 config: cty.ObjectVal(map[string]cty.Value{ 182 "hostname": cty.StringVal("localhost"), 183 "organization": cty.StringVal("hashicorp"), 184 "token": cty.NullVal(cty.String), 185 "workspaces": cty.ObjectVal(map[string]cty.Value{ 186 "name": cty.StringVal("prod"), 187 "tags": cty.NullVal(cty.Set(cty.String)), 188 }), 189 }), 190 confErr: "terraform login localhost", 191 }, 192 "with_tags": { 193 config: cty.ObjectVal(map[string]cty.Value{ 194 "hostname": cty.NullVal(cty.String), 195 "organization": cty.StringVal("hashicorp"), 196 "token": cty.NullVal(cty.String), 197 "workspaces": cty.ObjectVal(map[string]cty.Value{ 198 "name": cty.NullVal(cty.String), 199 "tags": cty.SetVal( 200 []cty.Value{ 201 cty.StringVal("billing"), 202 }, 203 ), 204 }), 205 }), 206 }, 207 "with_a_name": { 208 config: cty.ObjectVal(map[string]cty.Value{ 209 "hostname": cty.NullVal(cty.String), 210 "organization": cty.StringVal("hashicorp"), 211 "token": cty.NullVal(cty.String), 212 "workspaces": cty.ObjectVal(map[string]cty.Value{ 213 "name": cty.StringVal("prod"), 214 "tags": cty.NullVal(cty.Set(cty.String)), 215 }), 216 }), 217 }, 218 "without_a_name_tags": { 219 config: cty.ObjectVal(map[string]cty.Value{ 220 "hostname": cty.NullVal(cty.String), 221 "organization": cty.StringVal("hashicorp"), 222 "token": cty.NullVal(cty.String), 223 "workspaces": cty.ObjectVal(map[string]cty.Value{ 224 "name": cty.NullVal(cty.String), 225 "tags": cty.NullVal(cty.Set(cty.String)), 226 }), 227 }), 228 valErr: `Missing workspace mapping strategy.`, 229 }, 230 "with_both_a_name_and_tags": { 231 config: cty.ObjectVal(map[string]cty.Value{ 232 "hostname": cty.NullVal(cty.String), 233 "organization": cty.StringVal("hashicorp"), 234 "token": cty.NullVal(cty.String), 235 "workspaces": cty.ObjectVal(map[string]cty.Value{ 236 "name": cty.StringVal("prod"), 237 "tags": cty.SetVal( 238 []cty.Value{ 239 cty.StringVal("billing"), 240 }, 241 ), 242 }), 243 }), 244 valErr: `Only one of workspace "tags" or "name" is allowed.`, 245 }, 246 "null config": { 247 config: cty.NullVal(cty.EmptyObject), 248 }, 249 } 250 251 for name, tc := range cases { 252 s := testServer(t) 253 b := New(testDisco(s)) 254 255 // Validate 256 _, valDiags := b.PrepareConfig(tc.config) 257 if (valDiags.Err() != nil || tc.valErr != "") && 258 (valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) { 259 t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) 260 } 261 262 // Configure 263 confDiags := b.Configure(tc.config) 264 if (confDiags.Err() != nil || tc.confErr != "") && 265 (confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) { 266 t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err()) 267 } 268 } 269 } 270 271 func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) { 272 config := cty.ObjectVal(map[string]cty.Value{ 273 "hostname": cty.NullVal(cty.String), 274 "organization": cty.StringVal("hashicorp"), 275 "token": cty.NullVal(cty.String), 276 "workspaces": cty.ObjectVal(map[string]cty.Value{ 277 "name": cty.NullVal(cty.String), 278 "tags": cty.SetVal( 279 []cty.Value{ 280 cty.StringVal("billing"), 281 }, 282 ), 283 }), 284 }) 285 286 handlers := map[string]func(http.ResponseWriter, *http.Request){ 287 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 288 w.Header().Set("Content-Type", "application/json") 289 w.Header().Set("TFP-API-Version", "2.4") 290 }, 291 } 292 s := testServerWithHandlers(handlers) 293 294 b := New(testDisco(s)) 295 296 confDiags := b.Configure(config) 297 if confDiags.Err() == nil { 298 t.Fatalf("expected configure to error") 299 } 300 301 expected := `The 'cloud' option is not supported with this version of Terraform Enterprise.` 302 if !strings.Contains(confDiags.Err().Error(), expected) { 303 t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) 304 } 305 } 306 307 func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) { 308 config := cty.ObjectVal(map[string]cty.Value{ 309 "hostname": cty.NullVal(cty.String), 310 "organization": cty.StringVal("hashicorp"), 311 "token": cty.NullVal(cty.String), 312 "workspaces": cty.ObjectVal(map[string]cty.Value{ 313 "name": cty.NullVal(cty.String), 314 "tags": cty.SetVal( 315 []cty.Value{ 316 cty.StringVal("billing"), 317 }, 318 ), 319 }), 320 }) 321 322 handlers := map[string]func(http.ResponseWriter, *http.Request){ 323 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 324 w.Header().Set("Content-Type", "application/json") 325 w.Header().Set("TFP-API-Version", "2.4") 326 }, 327 } 328 s := testServerWithHandlers(handlers) 329 330 b := New(testDisco(s)) 331 b.runningInAutomation = true 332 333 confDiags := b.Configure(config) 334 if confDiags.Err() == nil { 335 t.Fatalf("expected configure to error") 336 } 337 338 expected := `This version of Terraform Cloud/Enterprise does not support the state mechanism 339 attempting to be used by the platform. This should never happen.` 340 if !strings.Contains(confDiags.Err().Error(), expected) { 341 t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) 342 } 343 } 344 345 func TestCloud_setUnavailableTerraformVersion(t *testing.T) { 346 // go-tfe returns an error IRL if you try to set a Terraform version that's 347 // not available in your TFC instance. To test this, tfe_client_mock errors if 348 // you try to set any Terraform version for this specific workspace name. 349 workspaceName := "unavailable-terraform-version" 350 351 config := cty.ObjectVal(map[string]cty.Value{ 352 "hostname": cty.NullVal(cty.String), 353 "organization": cty.StringVal("hashicorp"), 354 "token": cty.NullVal(cty.String), 355 "workspaces": cty.ObjectVal(map[string]cty.Value{ 356 "name": cty.NullVal(cty.String), 357 "tags": cty.SetVal( 358 []cty.Value{ 359 cty.StringVal("sometag"), 360 }, 361 ), 362 }), 363 }) 364 365 b, bCleanup := testBackend(t, config) 366 defer bCleanup() 367 368 // Make sure the workspace doesn't exist yet -- otherwise, we can't test what 369 // happens when a workspace gets created. This is why we can't use "name" in 370 // the backend config above, btw: if you do, testBackend() creates the default 371 // workspace before we get a chance to do anything. 372 _, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) 373 if err != tfe.ErrResourceNotFound { 374 t.Fatalf("the workspace we were about to try and create (%s/%s) already exists in the mocks somehow, so this test isn't trustworthy anymore", b.organization, workspaceName) 375 } 376 377 _, err = b.StateMgr(workspaceName) 378 if err != nil { 379 t.Fatalf("expected no error from StateMgr, despite not being able to set remote Terraform version: %#v", err) 380 } 381 // Make sure the workspace was created: 382 workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) 383 if err != nil { 384 t.Fatalf("b.StateMgr() didn't actually create the desired workspace") 385 } 386 // Make sure our mocks still error as expected, using the same update function b.StateMgr() would call: 387 _, err = b.client.Workspaces.UpdateByID( 388 context.Background(), 389 workspace.ID, 390 tfe.WorkspaceUpdateOptions{TerraformVersion: tfe.String("1.1.0")}, 391 ) 392 if err == nil { 393 t.Fatalf("the mocks aren't emulating a nonexistent remote Terraform version correctly, so this test isn't trustworthy anymore") 394 } 395 } 396 397 func TestCloud_setConfigurationFields(t *testing.T) { 398 originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND") 399 400 cases := map[string]struct { 401 obj cty.Value 402 expectedHostname string 403 expectedOrganziation string 404 expectedWorkspaceName string 405 expectedWorkspaceTags []string 406 expectedForceLocal bool 407 setEnv func() 408 resetEnv func() 409 expectedErr string 410 }{ 411 "with hostname set": { 412 obj: cty.ObjectVal(map[string]cty.Value{ 413 "organization": cty.StringVal("hashicorp"), 414 "hostname": cty.StringVal("hashicorp.com"), 415 "workspaces": cty.ObjectVal(map[string]cty.Value{ 416 "name": cty.StringVal("prod"), 417 "tags": cty.NullVal(cty.Set(cty.String)), 418 }), 419 }), 420 expectedHostname: "hashicorp.com", 421 expectedOrganziation: "hashicorp", 422 }, 423 "with hostname not set, set to default hostname": { 424 obj: cty.ObjectVal(map[string]cty.Value{ 425 "organization": cty.StringVal("hashicorp"), 426 "hostname": cty.NullVal(cty.String), 427 "workspaces": cty.ObjectVal(map[string]cty.Value{ 428 "name": cty.StringVal("prod"), 429 "tags": cty.NullVal(cty.Set(cty.String)), 430 }), 431 }), 432 expectedHostname: defaultHostname, 433 expectedOrganziation: "hashicorp", 434 }, 435 "with workspace name set": { 436 obj: cty.ObjectVal(map[string]cty.Value{ 437 "organization": cty.StringVal("hashicorp"), 438 "hostname": cty.StringVal("hashicorp.com"), 439 "workspaces": cty.ObjectVal(map[string]cty.Value{ 440 "name": cty.StringVal("prod"), 441 "tags": cty.NullVal(cty.Set(cty.String)), 442 }), 443 }), 444 expectedHostname: "hashicorp.com", 445 expectedOrganziation: "hashicorp", 446 expectedWorkspaceName: "prod", 447 }, 448 "with workspace tags set": { 449 obj: cty.ObjectVal(map[string]cty.Value{ 450 "organization": cty.StringVal("hashicorp"), 451 "hostname": cty.StringVal("hashicorp.com"), 452 "workspaces": cty.ObjectVal(map[string]cty.Value{ 453 "name": cty.NullVal(cty.String), 454 "tags": cty.SetVal( 455 []cty.Value{ 456 cty.StringVal("billing"), 457 }, 458 ), 459 }), 460 }), 461 expectedHostname: "hashicorp.com", 462 expectedOrganziation: "hashicorp", 463 expectedWorkspaceTags: []string{"billing"}, 464 }, 465 "with force local set": { 466 obj: cty.ObjectVal(map[string]cty.Value{ 467 "organization": cty.StringVal("hashicorp"), 468 "hostname": cty.StringVal("hashicorp.com"), 469 "workspaces": cty.ObjectVal(map[string]cty.Value{ 470 "name": cty.NullVal(cty.String), 471 "tags": cty.NullVal(cty.Set(cty.String)), 472 }), 473 }), 474 expectedHostname: "hashicorp.com", 475 expectedOrganziation: "hashicorp", 476 setEnv: func() { 477 os.Setenv("TF_FORCE_LOCAL_BACKEND", "1") 478 }, 479 resetEnv: func() { 480 os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv) 481 }, 482 expectedForceLocal: true, 483 }, 484 } 485 486 for name, tc := range cases { 487 b := &Cloud{} 488 489 // if `setEnv` is set, then we expect `resetEnv` to also be set 490 if tc.setEnv != nil { 491 tc.setEnv() 492 defer tc.resetEnv() 493 } 494 495 errDiags := b.setConfigurationFields(tc.obj) 496 if errDiags.HasErrors() || tc.expectedErr != "" { 497 actualErr := errDiags.Err().Error() 498 if !strings.Contains(actualErr, tc.expectedErr) { 499 t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err()) 500 } 501 } 502 503 if tc.expectedHostname != "" && b.hostname != tc.expectedHostname { 504 t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname) 505 } 506 if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation { 507 t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation) 508 } 509 if tc.expectedWorkspaceName != "" && b.WorkspaceMapping.Name != tc.expectedWorkspaceName { 510 t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName) 511 } 512 if len(tc.expectedWorkspaceTags) > 0 { 513 presentSet := make(map[string]struct{}) 514 for _, tag := range b.WorkspaceMapping.Tags { 515 presentSet[tag] = struct{}{} 516 } 517 518 expectedSet := make(map[string]struct{}) 519 for _, tag := range tc.expectedWorkspaceTags { 520 expectedSet[tag] = struct{}{} 521 } 522 523 var missing []string 524 var unexpected []string 525 526 for _, expected := range tc.expectedWorkspaceTags { 527 if _, ok := presentSet[expected]; !ok { 528 missing = append(missing, expected) 529 } 530 } 531 532 for _, actual := range b.WorkspaceMapping.Tags { 533 if _, ok := expectedSet[actual]; !ok { 534 unexpected = append(missing, actual) 535 } 536 } 537 538 if len(missing) > 0 { 539 t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.WorkspaceMapping.Tags, missing) 540 } 541 542 if len(unexpected) > 0 { 543 t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.WorkspaceMapping.Tags, unexpected) 544 } 545 546 } 547 if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal { 548 t.Fatalf("%s: expected force local backend to be set ", name) 549 } 550 } 551 } 552 553 func TestCloud_localBackend(t *testing.T) { 554 b, bCleanup := testBackendWithName(t) 555 defer bCleanup() 556 557 local, ok := b.local.(*backendLocal.Local) 558 if !ok { 559 t.Fatalf("expected b.local to be \"*local.Local\", got: %T", b.local) 560 } 561 562 cloud, ok := local.Backend.(*Cloud) 563 if !ok { 564 t.Fatalf("expected local.Backend to be *cloud.Cloud, got: %T", cloud) 565 } 566 } 567 568 func TestCloud_addAndRemoveWorkspacesDefault(t *testing.T) { 569 b, bCleanup := testBackendWithName(t) 570 defer bCleanup() 571 572 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 573 t.Fatalf("expected no error, got %v", err) 574 } 575 576 if err := b.DeleteWorkspace(testBackendSingleWorkspaceName); err != backend.ErrWorkspacesNotSupported { 577 t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err) 578 } 579 } 580 581 func TestCloud_StateMgr_versionCheck(t *testing.T) { 582 b, bCleanup := testBackendWithName(t) 583 defer bCleanup() 584 585 // Some fixed versions for testing with. This logic is a simple string 586 // comparison, so we don't need many test cases. 587 v0135 := version.Must(version.NewSemver("0.13.5")) 588 v0140 := version.Must(version.NewSemver("0.14.0")) 589 590 // Save original local version state and restore afterwards 591 p := tfversion.Prerelease 592 v := tfversion.Version 593 s := tfversion.SemVer 594 defer func() { 595 tfversion.Prerelease = p 596 tfversion.Version = v 597 tfversion.SemVer = s 598 }() 599 600 // For this test, the local Terraform version is set to 0.14.0 601 tfversion.Prerelease = "" 602 tfversion.Version = v0140.String() 603 tfversion.SemVer = v0140 604 605 // Update the mock remote workspace Terraform version to match the local 606 // Terraform version 607 if _, err := b.client.Workspaces.Update( 608 context.Background(), 609 b.organization, 610 b.WorkspaceMapping.Name, 611 tfe.WorkspaceUpdateOptions{ 612 TerraformVersion: tfe.String(v0140.String()), 613 }, 614 ); err != nil { 615 t.Fatalf("error: %v", err) 616 } 617 618 // This should succeed 619 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 620 t.Fatalf("expected no error, got %v", err) 621 } 622 623 // Now change the remote workspace to a different Terraform version 624 if _, err := b.client.Workspaces.Update( 625 context.Background(), 626 b.organization, 627 b.WorkspaceMapping.Name, 628 tfe.WorkspaceUpdateOptions{ 629 TerraformVersion: tfe.String(v0135.String()), 630 }, 631 ); err != nil { 632 t.Fatalf("error: %v", err) 633 } 634 635 // This should fail 636 want := `Remote workspace Terraform version "0.13.5" does not match local Terraform version "0.14.0"` 637 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err.Error() != want { 638 t.Fatalf("wrong error\n got: %v\nwant: %v", err.Error(), want) 639 } 640 } 641 642 func TestCloud_StateMgr_versionCheckLatest(t *testing.T) { 643 b, bCleanup := testBackendWithName(t) 644 defer bCleanup() 645 646 v0140 := version.Must(version.NewSemver("0.14.0")) 647 648 // Save original local version state and restore afterwards 649 p := tfversion.Prerelease 650 v := tfversion.Version 651 s := tfversion.SemVer 652 defer func() { 653 tfversion.Prerelease = p 654 tfversion.Version = v 655 tfversion.SemVer = s 656 }() 657 658 // For this test, the local Terraform version is set to 0.14.0 659 tfversion.Prerelease = "" 660 tfversion.Version = v0140.String() 661 tfversion.SemVer = v0140 662 663 // Update the remote workspace to the pseudo-version "latest" 664 if _, err := b.client.Workspaces.Update( 665 context.Background(), 666 b.organization, 667 b.WorkspaceMapping.Name, 668 tfe.WorkspaceUpdateOptions{ 669 TerraformVersion: tfe.String("latest"), 670 }, 671 ); err != nil { 672 t.Fatalf("error: %v", err) 673 } 674 675 // This should succeed despite not being a string match 676 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 677 t.Fatalf("expected no error, got %v", err) 678 } 679 } 680 681 func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) { 682 testCases := []struct { 683 local string 684 remote string 685 executionMode string 686 wantErr bool 687 }{ 688 {"0.13.5", "0.13.5", "remote", false}, 689 {"0.14.0", "0.13.5", "remote", true}, 690 {"0.14.0", "0.13.5", "local", false}, 691 {"0.14.0", "0.14.1", "remote", false}, 692 {"0.14.0", "1.0.99", "remote", false}, 693 {"0.14.0", "1.1.0", "remote", false}, 694 {"0.14.0", "1.2.0", "remote", true}, 695 {"1.2.0", "1.2.99", "remote", false}, 696 {"1.2.0", "1.3.0", "remote", true}, 697 {"0.15.0", "latest", "remote", false}, 698 {"1.1.5", "~> 1.1.1", "remote", false}, 699 {"1.1.5", "> 1.1.0, < 1.3.0", "remote", false}, 700 {"1.1.5", "~> 1.0.1", "remote", true}, 701 // pre-release versions are comparable within their pre-release stage (dev, 702 // alpha, beta), but not comparable to different stages and not comparable 703 // to final releases. 704 {"1.1.0-beta1", "1.1.0-beta1", "remote", false}, 705 {"1.1.0-beta1", "~> 1.1.0-beta", "remote", false}, 706 {"1.1.0", "~> 1.1.0-beta", "remote", true}, 707 {"1.1.0-beta1", "~> 1.1.0-dev", "remote", true}, 708 } 709 for _, tc := range testCases { 710 t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) { 711 b, bCleanup := testBackendWithName(t) 712 defer bCleanup() 713 714 local := version.Must(version.NewSemver(tc.local)) 715 716 // Save original local version state and restore afterwards 717 p := tfversion.Prerelease 718 v := tfversion.Version 719 s := tfversion.SemVer 720 defer func() { 721 tfversion.Prerelease = p 722 tfversion.Version = v 723 tfversion.SemVer = s 724 }() 725 726 // Override local version as specified 727 tfversion.Prerelease = "" 728 tfversion.Version = local.String() 729 tfversion.SemVer = local 730 731 // Update the mock remote workspace Terraform version to the 732 // specified remote version 733 if _, err := b.client.Workspaces.Update( 734 context.Background(), 735 b.organization, 736 b.WorkspaceMapping.Name, 737 tfe.WorkspaceUpdateOptions{ 738 ExecutionMode: &tc.executionMode, 739 TerraformVersion: tfe.String(tc.remote), 740 }, 741 ); err != nil { 742 t.Fatalf("error: %v", err) 743 } 744 745 diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 746 if tc.wantErr { 747 if len(diags) != 1 { 748 t.Fatal("expected diag, but none returned") 749 } 750 if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version") { 751 t.Fatalf("unexpected error: %s", got) 752 } 753 } else { 754 if len(diags) != 0 { 755 t.Fatalf("unexpected diags: %s", diags.Err()) 756 } 757 } 758 }) 759 } 760 } 761 762 func TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) { 763 b, bCleanup := testBackendWithName(t) 764 defer bCleanup() 765 766 // Attempting to check the version against a workspace which doesn't exist 767 // should result in no errors 768 diags := b.VerifyWorkspaceTerraformVersion("invalid-workspace") 769 if len(diags) != 0 { 770 t.Fatalf("unexpected error: %s", diags.Err()) 771 } 772 773 // Use a special workspace ID to trigger a 500 error, which should result 774 // in a failed check 775 diags = b.VerifyWorkspaceTerraformVersion("network-error") 776 if len(diags) != 1 { 777 t.Fatal("expected diag, but none returned") 778 } 779 if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Workspace read failed") { 780 t.Fatalf("unexpected error: %s", got) 781 } 782 783 // Update the mock remote workspace Terraform version to an invalid version 784 if _, err := b.client.Workspaces.Update( 785 context.Background(), 786 b.organization, 787 b.WorkspaceMapping.Name, 788 tfe.WorkspaceUpdateOptions{ 789 TerraformVersion: tfe.String("1.0.cheetarah"), 790 }, 791 ); err != nil { 792 t.Fatalf("error: %v", err) 793 } 794 diags = b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 795 796 if len(diags) != 1 { 797 t.Fatal("expected diag, but none returned") 798 } 799 if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version: The remote workspace specified") { 800 t.Fatalf("unexpected error: %s", got) 801 } 802 } 803 804 func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { 805 b, bCleanup := testBackendWithName(t) 806 defer bCleanup() 807 808 // If the ignore flag is set, the behaviour changes 809 b.IgnoreVersionConflict() 810 811 // Different local & remote versions to cause an error 812 local := version.Must(version.NewSemver("0.14.0")) 813 remote := version.Must(version.NewSemver("0.13.5")) 814 815 // Save original local version state and restore afterwards 816 p := tfversion.Prerelease 817 v := tfversion.Version 818 s := tfversion.SemVer 819 defer func() { 820 tfversion.Prerelease = p 821 tfversion.Version = v 822 tfversion.SemVer = s 823 }() 824 825 // Override local version as specified 826 tfversion.Prerelease = "" 827 tfversion.Version = local.String() 828 tfversion.SemVer = local 829 830 // Update the mock remote workspace Terraform version to the 831 // specified remote version 832 if _, err := b.client.Workspaces.Update( 833 context.Background(), 834 b.organization, 835 b.WorkspaceMapping.Name, 836 tfe.WorkspaceUpdateOptions{ 837 TerraformVersion: tfe.String(remote.String()), 838 }, 839 ); err != nil { 840 t.Fatalf("error: %v", err) 841 } 842 843 diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 844 if len(diags) != 1 { 845 t.Fatal("expected diag, but none returned") 846 } 847 848 if got, want := diags[0].Severity(), tfdiags.Warning; got != want { 849 t.Errorf("wrong severity: got %#v, want %#v", got, want) 850 } 851 if got, want := diags[0].Description().Summary, "Incompatible Terraform version"; got != want { 852 t.Errorf("wrong summary: got %s, want %s", got, want) 853 } 854 wantDetail := "The local Terraform version (0.14.0) does not meet the version requirements for remote workspace hashicorp/app-prod (0.13.5)." 855 if got := diags[0].Description().Detail; got != wantDetail { 856 t.Errorf("wrong summary: got %s, want %s", got, wantDetail) 857 } 858 }