github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/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/hashicorp/terraform/internal/backend" 14 "github.com/hashicorp/terraform/internal/tfdiags" 15 tfversion "github.com/hashicorp/terraform/version" 16 "github.com/zclconf/go-cty/cty" 17 18 backendLocal "github.com/hashicorp/terraform/internal/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 or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`, 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_PrepareConfigWithEnvVars(t *testing.T) { 150 cases := map[string]struct { 151 config cty.Value 152 vars map[string]string 153 expectedErr string 154 }{ 155 "with no organization": { 156 config: cty.ObjectVal(map[string]cty.Value{ 157 "organization": cty.NullVal(cty.String), 158 "workspaces": cty.ObjectVal(map[string]cty.Value{ 159 "name": cty.StringVal("prod"), 160 "tags": cty.NullVal(cty.Set(cty.String)), 161 }), 162 }), 163 vars: map[string]string{ 164 "TF_CLOUD_ORGANIZATION": "example-org", 165 }, 166 }, 167 "with no organization attribute or env var": { 168 config: cty.ObjectVal(map[string]cty.Value{ 169 "organization": cty.NullVal(cty.String), 170 "workspaces": cty.ObjectVal(map[string]cty.Value{ 171 "name": cty.StringVal("prod"), 172 "tags": cty.NullVal(cty.Set(cty.String)), 173 }), 174 }), 175 vars: map[string]string{}, 176 expectedErr: `Invalid or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`, 177 }, 178 "null workspace": { 179 config: cty.ObjectVal(map[string]cty.Value{ 180 "organization": cty.StringVal("hashicorp"), 181 "workspaces": cty.NullVal(cty.String), 182 }), 183 vars: map[string]string{ 184 "TF_WORKSPACE": "my-workspace", 185 }, 186 }, 187 "organization and workspace env var": { 188 config: cty.ObjectVal(map[string]cty.Value{ 189 "organization": cty.NullVal(cty.String), 190 "workspaces": cty.NullVal(cty.String), 191 }), 192 vars: map[string]string{ 193 "TF_CLOUD_ORGANIZATION": "hashicorp", 194 "TF_WORKSPACE": "my-workspace", 195 }, 196 }, 197 } 198 199 for name, tc := range cases { 200 t.Run(name, func(t *testing.T) { 201 s := testServer(t) 202 b := New(testDisco(s)) 203 204 for k, v := range tc.vars { 205 os.Setenv(k, v) 206 } 207 t.Cleanup(func() { 208 for k := range tc.vars { 209 os.Unsetenv(k) 210 } 211 }) 212 213 _, valDiags := b.PrepareConfig(tc.config) 214 if valDiags.Err() != nil && tc.expectedErr != "" { 215 actualErr := valDiags.Err().Error() 216 if !strings.Contains(actualErr, tc.expectedErr) { 217 t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) 218 } 219 } 220 }) 221 } 222 } 223 224 func TestCloud_configWithEnvVars(t *testing.T) { 225 cases := map[string]struct { 226 setup func(b *Cloud) 227 config cty.Value 228 vars map[string]string 229 expectedOrganization string 230 expectedHostname string 231 expectedWorkspaceName string 232 expectedErr string 233 }{ 234 "with no organization specified": { 235 config: cty.ObjectVal(map[string]cty.Value{ 236 "hostname": cty.NullVal(cty.String), 237 "token": cty.NullVal(cty.String), 238 "organization": cty.NullVal(cty.String), 239 "workspaces": cty.ObjectVal(map[string]cty.Value{ 240 "name": cty.StringVal("prod"), 241 "tags": cty.NullVal(cty.Set(cty.String)), 242 }), 243 }), 244 vars: map[string]string{ 245 "TF_CLOUD_ORGANIZATION": "hashicorp", 246 }, 247 expectedOrganization: "hashicorp", 248 }, 249 "with both organization and env var specified": { 250 config: cty.ObjectVal(map[string]cty.Value{ 251 "hostname": cty.NullVal(cty.String), 252 "token": cty.NullVal(cty.String), 253 "organization": cty.StringVal("hashicorp"), 254 "workspaces": cty.ObjectVal(map[string]cty.Value{ 255 "name": cty.StringVal("prod"), 256 "tags": cty.NullVal(cty.Set(cty.String)), 257 }), 258 }), 259 vars: map[string]string{ 260 "TF_CLOUD_ORGANIZATION": "we-should-not-see-this", 261 }, 262 expectedOrganization: "hashicorp", 263 }, 264 "with no hostname specified": { 265 config: cty.ObjectVal(map[string]cty.Value{ 266 "hostname": cty.NullVal(cty.String), 267 "token": cty.NullVal(cty.String), 268 "organization": cty.StringVal("hashicorp"), 269 "workspaces": cty.ObjectVal(map[string]cty.Value{ 270 "name": cty.StringVal("prod"), 271 "tags": cty.NullVal(cty.Set(cty.String)), 272 }), 273 }), 274 vars: map[string]string{ 275 "TF_CLOUD_HOSTNAME": "private.hashicorp.engineering", 276 }, 277 expectedHostname: "private.hashicorp.engineering", 278 }, 279 "with hostname and env var specified": { 280 config: cty.ObjectVal(map[string]cty.Value{ 281 "hostname": cty.StringVal("private.hashicorp.engineering"), 282 "token": cty.NullVal(cty.String), 283 "organization": cty.StringVal("hashicorp"), 284 "workspaces": cty.ObjectVal(map[string]cty.Value{ 285 "name": cty.StringVal("prod"), 286 "tags": cty.NullVal(cty.Set(cty.String)), 287 }), 288 }), 289 vars: map[string]string{ 290 "TF_CLOUD_HOSTNAME": "mycool.tfe-host.io", 291 }, 292 expectedHostname: "private.hashicorp.engineering", 293 }, 294 "an invalid workspace env var": { 295 config: cty.ObjectVal(map[string]cty.Value{ 296 "hostname": cty.NullVal(cty.String), 297 "token": cty.NullVal(cty.String), 298 "organization": cty.StringVal("hashicorp"), 299 "workspaces": cty.NullVal(cty.Object(map[string]cty.Type{ 300 "name": cty.String, 301 "tags": cty.Set(cty.String), 302 })), 303 }), 304 vars: map[string]string{ 305 "TF_WORKSPACE": "i-dont-exist-in-org", 306 }, 307 expectedErr: `Invalid workspace selection: Terraform failed to find workspace "i-dont-exist-in-org" in organization hashicorp`, 308 }, 309 "workspaces and env var specified": { 310 config: cty.ObjectVal(map[string]cty.Value{ 311 "hostname": cty.NullVal(cty.String), 312 "token": cty.NullVal(cty.String), 313 "organization": cty.StringVal("mordor"), 314 "workspaces": cty.ObjectVal(map[string]cty.Value{ 315 "name": cty.StringVal("mt-doom"), 316 "tags": cty.NullVal(cty.Set(cty.String)), 317 }), 318 }), 319 vars: map[string]string{ 320 "TF_WORKSPACE": "shire", 321 }, 322 expectedWorkspaceName: "mt-doom", 323 }, 324 "env var workspace does not have specified tag": { 325 setup: func(b *Cloud) { 326 b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ 327 Name: tfe.String("mordor"), 328 }) 329 330 b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{ 331 Name: tfe.String("shire"), 332 }) 333 }, 334 config: cty.ObjectVal(map[string]cty.Value{ 335 "hostname": cty.NullVal(cty.String), 336 "token": cty.NullVal(cty.String), 337 "organization": cty.StringVal("mordor"), 338 "workspaces": cty.ObjectVal(map[string]cty.Value{ 339 "name": cty.NullVal(cty.String), 340 "tags": cty.SetVal([]cty.Value{ 341 cty.StringVal("cloud"), 342 }), 343 }), 344 }), 345 vars: map[string]string{ 346 "TF_WORKSPACE": "shire", 347 }, 348 expectedErr: "Terraform failed to find workspace \"shire\" with the tags specified in your configuration:\n[cloud]", 349 }, 350 "env var workspace has specified tag": { 351 setup: func(b *Cloud) { 352 b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ 353 Name: tfe.String("mordor"), 354 }) 355 356 b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{ 357 Name: tfe.String("shire"), 358 Tags: []*tfe.Tag{ 359 { 360 Name: "hobbity", 361 }, 362 }, 363 }) 364 }, 365 config: cty.ObjectVal(map[string]cty.Value{ 366 "hostname": cty.NullVal(cty.String), 367 "token": cty.NullVal(cty.String), 368 "organization": cty.StringVal("mordor"), 369 "workspaces": cty.ObjectVal(map[string]cty.Value{ 370 "name": cty.NullVal(cty.String), 371 "tags": cty.SetVal([]cty.Value{ 372 cty.StringVal("hobbity"), 373 }), 374 }), 375 }), 376 vars: map[string]string{ 377 "TF_WORKSPACE": "shire", 378 }, 379 expectedWorkspaceName: "", // No error is raised, but workspace is not set 380 }, 381 "with everything set as env vars": { 382 config: cty.ObjectVal(map[string]cty.Value{ 383 "hostname": cty.NullVal(cty.String), 384 "token": cty.NullVal(cty.String), 385 "organization": cty.NullVal(cty.String), 386 "workspaces": cty.NullVal(cty.String), 387 }), 388 vars: map[string]string{ 389 "TF_CLOUD_ORGANIZATION": "mordor", 390 "TF_WORKSPACE": "mt-doom", 391 "TF_CLOUD_HOSTNAME": "mycool.tfe-host.io", 392 }, 393 expectedOrganization: "mordor", 394 expectedWorkspaceName: "mt-doom", 395 expectedHostname: "mycool.tfe-host.io", 396 }, 397 } 398 399 for name, tc := range cases { 400 t.Run(name, func(t *testing.T) { 401 b, cleanup := testUnconfiguredBackend(t) 402 t.Cleanup(cleanup) 403 404 for k, v := range tc.vars { 405 os.Setenv(k, v) 406 } 407 408 t.Cleanup(func() { 409 for k := range tc.vars { 410 os.Unsetenv(k) 411 } 412 }) 413 414 _, valDiags := b.PrepareConfig(tc.config) 415 if valDiags.Err() != nil { 416 t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) 417 } 418 419 if tc.setup != nil { 420 tc.setup(b) 421 } 422 423 diags := b.Configure(tc.config) 424 if (diags.Err() != nil || tc.expectedErr != "") && 425 (diags.Err() == nil || !strings.Contains(diags.Err().Error(), tc.expectedErr)) { 426 t.Fatalf("%s: unexpected configure result: %v", name, diags.Err()) 427 } 428 429 if tc.expectedOrganization != "" && tc.expectedOrganization != b.organization { 430 t.Fatalf("%s: organization not valid: %s, expected: %s", name, b.organization, tc.expectedOrganization) 431 } 432 433 if tc.expectedHostname != "" && tc.expectedHostname != b.hostname { 434 t.Fatalf("%s: hostname not valid: %s, expected: %s", name, b.hostname, tc.expectedHostname) 435 } 436 437 if tc.expectedWorkspaceName != "" && tc.expectedWorkspaceName != b.WorkspaceMapping.Name { 438 t.Fatalf("%s: workspace name not valid: %s, expected: %s", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName) 439 } 440 }) 441 } 442 } 443 444 func TestCloud_config(t *testing.T) { 445 cases := map[string]struct { 446 config cty.Value 447 confErr string 448 valErr string 449 }{ 450 "with_an_unknown_host": { 451 config: cty.ObjectVal(map[string]cty.Value{ 452 "hostname": cty.StringVal("nonexisting.local"), 453 "organization": cty.StringVal("hashicorp"), 454 "token": cty.NullVal(cty.String), 455 "workspaces": cty.ObjectVal(map[string]cty.Value{ 456 "name": cty.StringVal("prod"), 457 "tags": cty.NullVal(cty.Set(cty.String)), 458 }), 459 }), 460 confErr: "Failed to request discovery document", 461 }, 462 // localhost advertises TFE services, but has no token in the credentials 463 "without_a_token": { 464 config: cty.ObjectVal(map[string]cty.Value{ 465 "hostname": cty.StringVal("localhost"), 466 "organization": cty.StringVal("hashicorp"), 467 "token": cty.NullVal(cty.String), 468 "workspaces": cty.ObjectVal(map[string]cty.Value{ 469 "name": cty.StringVal("prod"), 470 "tags": cty.NullVal(cty.Set(cty.String)), 471 }), 472 }), 473 confErr: "terraform login localhost", 474 }, 475 "with_tags": { 476 config: cty.ObjectVal(map[string]cty.Value{ 477 "hostname": cty.NullVal(cty.String), 478 "organization": cty.StringVal("hashicorp"), 479 "token": cty.NullVal(cty.String), 480 "workspaces": cty.ObjectVal(map[string]cty.Value{ 481 "name": cty.NullVal(cty.String), 482 "tags": cty.SetVal( 483 []cty.Value{ 484 cty.StringVal("billing"), 485 }, 486 ), 487 }), 488 }), 489 }, 490 "with_a_name": { 491 config: cty.ObjectVal(map[string]cty.Value{ 492 "hostname": cty.NullVal(cty.String), 493 "organization": cty.StringVal("hashicorp"), 494 "token": cty.NullVal(cty.String), 495 "workspaces": cty.ObjectVal(map[string]cty.Value{ 496 "name": cty.StringVal("prod"), 497 "tags": cty.NullVal(cty.Set(cty.String)), 498 }), 499 }), 500 }, 501 "without_a_name_tags": { 502 config: cty.ObjectVal(map[string]cty.Value{ 503 "hostname": cty.NullVal(cty.String), 504 "organization": cty.StringVal("hashicorp"), 505 "token": cty.NullVal(cty.String), 506 "workspaces": cty.ObjectVal(map[string]cty.Value{ 507 "name": cty.NullVal(cty.String), 508 "tags": cty.NullVal(cty.Set(cty.String)), 509 }), 510 }), 511 valErr: `Missing workspace mapping strategy.`, 512 }, 513 "with_both_a_name_and_tags": { 514 config: cty.ObjectVal(map[string]cty.Value{ 515 "hostname": cty.NullVal(cty.String), 516 "organization": cty.StringVal("hashicorp"), 517 "token": cty.NullVal(cty.String), 518 "workspaces": cty.ObjectVal(map[string]cty.Value{ 519 "name": cty.StringVal("prod"), 520 "tags": cty.SetVal( 521 []cty.Value{ 522 cty.StringVal("billing"), 523 }, 524 ), 525 }), 526 }), 527 valErr: `Only one of workspace "tags" or "name" is allowed.`, 528 }, 529 "null config": { 530 config: cty.NullVal(cty.EmptyObject), 531 }, 532 } 533 534 for name, tc := range cases { 535 b, cleanup := testUnconfiguredBackend(t) 536 t.Cleanup(cleanup) 537 538 // Validate 539 _, valDiags := b.PrepareConfig(tc.config) 540 if (valDiags.Err() != nil || tc.valErr != "") && 541 (valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) { 542 t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) 543 } 544 545 // Configure 546 confDiags := b.Configure(tc.config) 547 if (confDiags.Err() != nil || tc.confErr != "") && 548 (confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) { 549 t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err()) 550 } 551 } 552 } 553 554 func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) { 555 config := cty.ObjectVal(map[string]cty.Value{ 556 "hostname": cty.NullVal(cty.String), 557 "organization": cty.StringVal("hashicorp"), 558 "token": cty.NullVal(cty.String), 559 "workspaces": cty.ObjectVal(map[string]cty.Value{ 560 "name": cty.NullVal(cty.String), 561 "tags": cty.SetVal( 562 []cty.Value{ 563 cty.StringVal("billing"), 564 }, 565 ), 566 }), 567 }) 568 569 handlers := map[string]func(http.ResponseWriter, *http.Request){ 570 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 571 w.Header().Set("Content-Type", "application/json") 572 w.Header().Set("TFP-API-Version", "2.4") 573 }, 574 } 575 s := testServerWithHandlers(handlers) 576 577 b := New(testDisco(s)) 578 579 confDiags := b.Configure(config) 580 if confDiags.Err() == nil { 581 t.Fatalf("expected configure to error") 582 } 583 584 expected := `The 'cloud' option is not supported with this version of Terraform Enterprise.` 585 if !strings.Contains(confDiags.Err().Error(), expected) { 586 t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) 587 } 588 } 589 590 func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) { 591 config := cty.ObjectVal(map[string]cty.Value{ 592 "hostname": cty.NullVal(cty.String), 593 "organization": cty.StringVal("hashicorp"), 594 "token": cty.NullVal(cty.String), 595 "workspaces": cty.ObjectVal(map[string]cty.Value{ 596 "name": cty.NullVal(cty.String), 597 "tags": cty.SetVal( 598 []cty.Value{ 599 cty.StringVal("billing"), 600 }, 601 ), 602 }), 603 }) 604 605 handlers := map[string]func(http.ResponseWriter, *http.Request){ 606 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 607 w.Header().Set("Content-Type", "application/json") 608 w.Header().Set("TFP-API-Version", "2.4") 609 }, 610 } 611 s := testServerWithHandlers(handlers) 612 613 b := New(testDisco(s)) 614 b.runningInAutomation = true 615 616 confDiags := b.Configure(config) 617 if confDiags.Err() == nil { 618 t.Fatalf("expected configure to error") 619 } 620 621 expected := `This version of Terraform Cloud/Enterprise does not support the state mechanism 622 attempting to be used by the platform. This should never happen.` 623 if !strings.Contains(confDiags.Err().Error(), expected) { 624 t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) 625 } 626 } 627 628 func TestCloud_setUnavailableTerraformVersion(t *testing.T) { 629 // go-tfe returns an error IRL if you try to set a Terraform version that's 630 // not available in your TFC instance. To test this, tfe_client_mock errors if 631 // you try to set any Terraform version for this specific workspace name. 632 workspaceName := "unavailable-terraform-version" 633 634 config := cty.ObjectVal(map[string]cty.Value{ 635 "hostname": cty.NullVal(cty.String), 636 "organization": cty.StringVal("hashicorp"), 637 "token": cty.NullVal(cty.String), 638 "workspaces": cty.ObjectVal(map[string]cty.Value{ 639 "name": cty.NullVal(cty.String), 640 "tags": cty.SetVal( 641 []cty.Value{ 642 cty.StringVal("sometag"), 643 }, 644 ), 645 }), 646 }) 647 648 b, bCleanup := testBackend(t, config) 649 defer bCleanup() 650 651 // Make sure the workspace doesn't exist yet -- otherwise, we can't test what 652 // happens when a workspace gets created. This is why we can't use "name" in 653 // the backend config above, btw: if you do, testBackend() creates the default 654 // workspace before we get a chance to do anything. 655 _, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) 656 if err != tfe.ErrResourceNotFound { 657 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) 658 } 659 660 _, err = b.StateMgr(workspaceName) 661 if err != nil { 662 t.Fatalf("expected no error from StateMgr, despite not being able to set remote Terraform version: %#v", err) 663 } 664 // Make sure the workspace was created: 665 workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) 666 if err != nil { 667 t.Fatalf("b.StateMgr() didn't actually create the desired workspace") 668 } 669 // Make sure our mocks still error as expected, using the same update function b.StateMgr() would call: 670 _, err = b.client.Workspaces.UpdateByID( 671 context.Background(), 672 workspace.ID, 673 tfe.WorkspaceUpdateOptions{TerraformVersion: tfe.String("1.1.0")}, 674 ) 675 if err == nil { 676 t.Fatalf("the mocks aren't emulating a nonexistent remote Terraform version correctly, so this test isn't trustworthy anymore") 677 } 678 } 679 680 func TestCloud_setConfigurationFields(t *testing.T) { 681 originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND") 682 683 cases := map[string]struct { 684 obj cty.Value 685 expectedHostname string 686 expectedOrganziation string 687 expectedWorkspaceName string 688 expectedWorkspaceTags []string 689 expectedForceLocal bool 690 setEnv func() 691 resetEnv func() 692 expectedErr string 693 }{ 694 "with hostname set": { 695 obj: cty.ObjectVal(map[string]cty.Value{ 696 "organization": cty.StringVal("hashicorp"), 697 "hostname": cty.StringVal("hashicorp.com"), 698 "workspaces": cty.ObjectVal(map[string]cty.Value{ 699 "name": cty.StringVal("prod"), 700 "tags": cty.NullVal(cty.Set(cty.String)), 701 }), 702 }), 703 expectedHostname: "hashicorp.com", 704 expectedOrganziation: "hashicorp", 705 }, 706 "with hostname not set, set to default hostname": { 707 obj: cty.ObjectVal(map[string]cty.Value{ 708 "organization": cty.StringVal("hashicorp"), 709 "hostname": cty.NullVal(cty.String), 710 "workspaces": cty.ObjectVal(map[string]cty.Value{ 711 "name": cty.StringVal("prod"), 712 "tags": cty.NullVal(cty.Set(cty.String)), 713 }), 714 }), 715 expectedHostname: defaultHostname, 716 expectedOrganziation: "hashicorp", 717 }, 718 "with workspace name set": { 719 obj: cty.ObjectVal(map[string]cty.Value{ 720 "organization": cty.StringVal("hashicorp"), 721 "hostname": cty.StringVal("hashicorp.com"), 722 "workspaces": cty.ObjectVal(map[string]cty.Value{ 723 "name": cty.StringVal("prod"), 724 "tags": cty.NullVal(cty.Set(cty.String)), 725 }), 726 }), 727 expectedHostname: "hashicorp.com", 728 expectedOrganziation: "hashicorp", 729 expectedWorkspaceName: "prod", 730 }, 731 "with workspace tags set": { 732 obj: cty.ObjectVal(map[string]cty.Value{ 733 "organization": cty.StringVal("hashicorp"), 734 "hostname": cty.StringVal("hashicorp.com"), 735 "workspaces": cty.ObjectVal(map[string]cty.Value{ 736 "name": cty.NullVal(cty.String), 737 "tags": cty.SetVal( 738 []cty.Value{ 739 cty.StringVal("billing"), 740 }, 741 ), 742 }), 743 }), 744 expectedHostname: "hashicorp.com", 745 expectedOrganziation: "hashicorp", 746 expectedWorkspaceTags: []string{"billing"}, 747 }, 748 "with force local set": { 749 obj: cty.ObjectVal(map[string]cty.Value{ 750 "organization": cty.StringVal("hashicorp"), 751 "hostname": cty.StringVal("hashicorp.com"), 752 "workspaces": cty.ObjectVal(map[string]cty.Value{ 753 "name": cty.NullVal(cty.String), 754 "tags": cty.NullVal(cty.Set(cty.String)), 755 }), 756 }), 757 expectedHostname: "hashicorp.com", 758 expectedOrganziation: "hashicorp", 759 setEnv: func() { 760 os.Setenv("TF_FORCE_LOCAL_BACKEND", "1") 761 }, 762 resetEnv: func() { 763 os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv) 764 }, 765 expectedForceLocal: true, 766 }, 767 } 768 769 for name, tc := range cases { 770 b := &Cloud{} 771 772 // if `setEnv` is set, then we expect `resetEnv` to also be set 773 if tc.setEnv != nil { 774 tc.setEnv() 775 defer tc.resetEnv() 776 } 777 778 errDiags := b.setConfigurationFields(tc.obj) 779 if errDiags.HasErrors() || tc.expectedErr != "" { 780 actualErr := errDiags.Err().Error() 781 if !strings.Contains(actualErr, tc.expectedErr) { 782 t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err()) 783 } 784 } 785 786 if tc.expectedHostname != "" && b.hostname != tc.expectedHostname { 787 t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname) 788 } 789 if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation { 790 t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation) 791 } 792 if tc.expectedWorkspaceName != "" && b.WorkspaceMapping.Name != tc.expectedWorkspaceName { 793 t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName) 794 } 795 if len(tc.expectedWorkspaceTags) > 0 { 796 presentSet := make(map[string]struct{}) 797 for _, tag := range b.WorkspaceMapping.Tags { 798 presentSet[tag] = struct{}{} 799 } 800 801 expectedSet := make(map[string]struct{}) 802 for _, tag := range tc.expectedWorkspaceTags { 803 expectedSet[tag] = struct{}{} 804 } 805 806 var missing []string 807 var unexpected []string 808 809 for _, expected := range tc.expectedWorkspaceTags { 810 if _, ok := presentSet[expected]; !ok { 811 missing = append(missing, expected) 812 } 813 } 814 815 for _, actual := range b.WorkspaceMapping.Tags { 816 if _, ok := expectedSet[actual]; !ok { 817 unexpected = append(missing, actual) 818 } 819 } 820 821 if len(missing) > 0 { 822 t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.WorkspaceMapping.Tags, missing) 823 } 824 825 if len(unexpected) > 0 { 826 t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.WorkspaceMapping.Tags, unexpected) 827 } 828 829 } 830 if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal { 831 t.Fatalf("%s: expected force local backend to be set ", name) 832 } 833 } 834 } 835 836 func TestCloud_localBackend(t *testing.T) { 837 b, bCleanup := testBackendWithName(t) 838 defer bCleanup() 839 840 local, ok := b.local.(*backendLocal.Local) 841 if !ok { 842 t.Fatalf("expected b.local to be \"*local.Local\", got: %T", b.local) 843 } 844 845 cloud, ok := local.Backend.(*Cloud) 846 if !ok { 847 t.Fatalf("expected local.Backend to be *cloud.Cloud, got: %T", cloud) 848 } 849 } 850 851 func TestCloud_addAndRemoveWorkspacesDefault(t *testing.T) { 852 b, bCleanup := testBackendWithName(t) 853 defer bCleanup() 854 855 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 856 t.Fatalf("expected no error, got %v", err) 857 } 858 859 if err := b.DeleteWorkspace(testBackendSingleWorkspaceName); err != backend.ErrWorkspacesNotSupported { 860 t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err) 861 } 862 } 863 864 func TestCloud_StateMgr_versionCheck(t *testing.T) { 865 b, bCleanup := testBackendWithName(t) 866 defer bCleanup() 867 868 // Some fixed versions for testing with. This logic is a simple string 869 // comparison, so we don't need many test cases. 870 v0135 := version.Must(version.NewSemver("0.13.5")) 871 v0140 := version.Must(version.NewSemver("0.14.0")) 872 873 // Save original local version state and restore afterwards 874 p := tfversion.Prerelease 875 v := tfversion.Version 876 s := tfversion.SemVer 877 defer func() { 878 tfversion.Prerelease = p 879 tfversion.Version = v 880 tfversion.SemVer = s 881 }() 882 883 // For this test, the local Terraform version is set to 0.14.0 884 tfversion.Prerelease = "" 885 tfversion.Version = v0140.String() 886 tfversion.SemVer = v0140 887 888 // Update the mock remote workspace Terraform version to match the local 889 // Terraform version 890 if _, err := b.client.Workspaces.Update( 891 context.Background(), 892 b.organization, 893 b.WorkspaceMapping.Name, 894 tfe.WorkspaceUpdateOptions{ 895 TerraformVersion: tfe.String(v0140.String()), 896 }, 897 ); err != nil { 898 t.Fatalf("error: %v", err) 899 } 900 901 // This should succeed 902 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 903 t.Fatalf("expected no error, got %v", err) 904 } 905 906 // Now change the remote workspace to a different Terraform version 907 if _, err := b.client.Workspaces.Update( 908 context.Background(), 909 b.organization, 910 b.WorkspaceMapping.Name, 911 tfe.WorkspaceUpdateOptions{ 912 TerraformVersion: tfe.String(v0135.String()), 913 }, 914 ); err != nil { 915 t.Fatalf("error: %v", err) 916 } 917 918 // This should fail 919 want := `Remote workspace Terraform version "0.13.5" does not match local Terraform version "0.14.0"` 920 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err.Error() != want { 921 t.Fatalf("wrong error\n got: %v\nwant: %v", err.Error(), want) 922 } 923 } 924 925 func TestCloud_StateMgr_versionCheckLatest(t *testing.T) { 926 b, bCleanup := testBackendWithName(t) 927 defer bCleanup() 928 929 v0140 := version.Must(version.NewSemver("0.14.0")) 930 931 // Save original local version state and restore afterwards 932 p := tfversion.Prerelease 933 v := tfversion.Version 934 s := tfversion.SemVer 935 defer func() { 936 tfversion.Prerelease = p 937 tfversion.Version = v 938 tfversion.SemVer = s 939 }() 940 941 // For this test, the local Terraform version is set to 0.14.0 942 tfversion.Prerelease = "" 943 tfversion.Version = v0140.String() 944 tfversion.SemVer = v0140 945 946 // Update the remote workspace to the pseudo-version "latest" 947 if _, err := b.client.Workspaces.Update( 948 context.Background(), 949 b.organization, 950 b.WorkspaceMapping.Name, 951 tfe.WorkspaceUpdateOptions{ 952 TerraformVersion: tfe.String("latest"), 953 }, 954 ); err != nil { 955 t.Fatalf("error: %v", err) 956 } 957 958 // This should succeed despite not being a string match 959 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 960 t.Fatalf("expected no error, got %v", err) 961 } 962 } 963 964 func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) { 965 testCases := []struct { 966 local string 967 remote string 968 executionMode string 969 wantErr bool 970 }{ 971 {"0.13.5", "0.13.5", "agent", false}, 972 {"0.14.0", "0.13.5", "remote", true}, 973 {"0.14.0", "0.13.5", "local", false}, 974 {"0.14.0", "0.14.1", "remote", false}, 975 {"0.14.0", "1.0.99", "remote", false}, 976 {"0.14.0", "1.1.0", "remote", false}, 977 {"0.14.0", "1.3.0", "remote", true}, 978 {"1.2.0", "1.2.99", "remote", false}, 979 {"1.2.0", "1.3.0", "remote", true}, 980 {"0.15.0", "latest", "remote", false}, 981 {"1.1.5", "~> 1.1.1", "remote", false}, 982 {"1.1.5", "> 1.1.0, < 1.3.0", "remote", false}, 983 {"1.1.5", "~> 1.0.1", "remote", true}, 984 // pre-release versions are comparable within their pre-release stage (dev, 985 // alpha, beta), but not comparable to different stages and not comparable 986 // to final releases. 987 {"1.1.0-beta1", "1.1.0-beta1", "remote", false}, 988 {"1.1.0-beta1", "~> 1.1.0-beta", "remote", false}, 989 {"1.1.0", "~> 1.1.0-beta", "remote", true}, 990 {"1.1.0-beta1", "~> 1.1.0-dev", "remote", true}, 991 } 992 for _, tc := range testCases { 993 t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) { 994 b, bCleanup := testBackendWithName(t) 995 defer bCleanup() 996 997 local := version.Must(version.NewSemver(tc.local)) 998 999 // Save original local version state and restore afterwards 1000 p := tfversion.Prerelease 1001 v := tfversion.Version 1002 s := tfversion.SemVer 1003 defer func() { 1004 tfversion.Prerelease = p 1005 tfversion.Version = v 1006 tfversion.SemVer = s 1007 }() 1008 1009 // Override local version as specified 1010 tfversion.Prerelease = "" 1011 tfversion.Version = local.String() 1012 tfversion.SemVer = local 1013 1014 // Update the mock remote workspace Terraform version to the 1015 // specified remote version 1016 if _, err := b.client.Workspaces.Update( 1017 context.Background(), 1018 b.organization, 1019 b.WorkspaceMapping.Name, 1020 tfe.WorkspaceUpdateOptions{ 1021 ExecutionMode: &tc.executionMode, 1022 TerraformVersion: tfe.String(tc.remote), 1023 }, 1024 ); err != nil { 1025 t.Fatalf("error: %v", err) 1026 } 1027 1028 diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 1029 if tc.wantErr { 1030 if len(diags) != 1 { 1031 t.Fatal("expected diag, but none returned") 1032 } 1033 if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version") { 1034 t.Fatalf("unexpected error: %s", got) 1035 } 1036 } else { 1037 if len(diags) != 0 { 1038 t.Fatalf("unexpected diags: %s", diags.Err()) 1039 } 1040 } 1041 }) 1042 } 1043 } 1044 1045 func TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) { 1046 b, bCleanup := testBackendWithName(t) 1047 defer bCleanup() 1048 1049 // Attempting to check the version against a workspace which doesn't exist 1050 // should result in no errors 1051 diags := b.VerifyWorkspaceTerraformVersion("invalid-workspace") 1052 if len(diags) != 0 { 1053 t.Fatalf("unexpected error: %s", diags.Err()) 1054 } 1055 1056 // Use a special workspace ID to trigger a 500 error, which should result 1057 // in a failed check 1058 diags = b.VerifyWorkspaceTerraformVersion("network-error") 1059 if len(diags) != 1 { 1060 t.Fatal("expected diag, but none returned") 1061 } 1062 if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Workspace read failed") { 1063 t.Fatalf("unexpected error: %s", got) 1064 } 1065 1066 // Update the mock remote workspace Terraform version to an invalid version 1067 if _, err := b.client.Workspaces.Update( 1068 context.Background(), 1069 b.organization, 1070 b.WorkspaceMapping.Name, 1071 tfe.WorkspaceUpdateOptions{ 1072 TerraformVersion: tfe.String("1.0.cheetarah"), 1073 }, 1074 ); err != nil { 1075 t.Fatalf("error: %v", err) 1076 } 1077 diags = b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 1078 1079 if len(diags) != 1 { 1080 t.Fatal("expected diag, but none returned") 1081 } 1082 if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version: The remote workspace specified") { 1083 t.Fatalf("unexpected error: %s", got) 1084 } 1085 } 1086 1087 func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { 1088 b, bCleanup := testBackendWithName(t) 1089 defer bCleanup() 1090 1091 // If the ignore flag is set, the behaviour changes 1092 b.IgnoreVersionConflict() 1093 1094 // Different local & remote versions to cause an error 1095 local := version.Must(version.NewSemver("0.14.0")) 1096 remote := version.Must(version.NewSemver("0.13.5")) 1097 1098 // Save original local version state and restore afterwards 1099 p := tfversion.Prerelease 1100 v := tfversion.Version 1101 s := tfversion.SemVer 1102 defer func() { 1103 tfversion.Prerelease = p 1104 tfversion.Version = v 1105 tfversion.SemVer = s 1106 }() 1107 1108 // Override local version as specified 1109 tfversion.Prerelease = "" 1110 tfversion.Version = local.String() 1111 tfversion.SemVer = local 1112 1113 // Update the mock remote workspace Terraform version to the 1114 // specified remote version 1115 if _, err := b.client.Workspaces.Update( 1116 context.Background(), 1117 b.organization, 1118 b.WorkspaceMapping.Name, 1119 tfe.WorkspaceUpdateOptions{ 1120 TerraformVersion: tfe.String(remote.String()), 1121 }, 1122 ); err != nil { 1123 t.Fatalf("error: %v", err) 1124 } 1125 1126 diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 1127 if len(diags) != 1 { 1128 t.Fatal("expected diag, but none returned") 1129 } 1130 1131 if got, want := diags[0].Severity(), tfdiags.Warning; got != want { 1132 t.Errorf("wrong severity: got %#v, want %#v", got, want) 1133 } 1134 if got, want := diags[0].Description().Summary, "Incompatible Terraform version"; got != want { 1135 t.Errorf("wrong summary: got %s, want %s", got, want) 1136 } 1137 wantDetail := "The local Terraform version (0.14.0) does not meet the version requirements for remote workspace hashicorp/app-prod (0.13.5)." 1138 if got := diags[0].Description().Detail; got != wantDetail { 1139 t.Errorf("wrong summary: got %s, want %s", got, wantDetail) 1140 } 1141 }