github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/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, true); 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", true); 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_a_non_tfe_host": { 451 config: cty.ObjectVal(map[string]cty.Value{ 452 "hostname": cty.StringVal("nontfe.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: "Host nontfe.local does not provide a tfe service", 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 t.Run(name, func(t *testing.T) { 536 b, cleanup := testUnconfiguredBackend(t) 537 t.Cleanup(cleanup) 538 539 // Validate 540 _, valDiags := b.PrepareConfig(tc.config) 541 if (valDiags.Err() != nil || tc.valErr != "") && 542 (valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) { 543 t.Fatalf("unexpected validation result: %v", valDiags.Err()) 544 } 545 546 // Configure 547 confDiags := b.Configure(tc.config) 548 if (confDiags.Err() != nil || tc.confErr != "") && 549 (confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) { 550 t.Fatalf("unexpected configure result: %v", confDiags.Err()) 551 } 552 }) 553 } 554 } 555 556 func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) { 557 config := cty.ObjectVal(map[string]cty.Value{ 558 "hostname": cty.NullVal(cty.String), 559 "organization": cty.StringVal("hashicorp"), 560 "token": cty.NullVal(cty.String), 561 "workspaces": cty.ObjectVal(map[string]cty.Value{ 562 "name": cty.NullVal(cty.String), 563 "tags": cty.SetVal( 564 []cty.Value{ 565 cty.StringVal("billing"), 566 }, 567 ), 568 }), 569 }) 570 571 handlers := map[string]func(http.ResponseWriter, *http.Request){ 572 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 573 w.Header().Set("Content-Type", "application/json") 574 w.Header().Set("TFP-API-Version", "2.4") 575 }, 576 } 577 s := testServerWithHandlers(handlers) 578 579 b := New(testDisco(s)) 580 581 confDiags := b.Configure(config) 582 if confDiags.Err() == nil { 583 t.Fatalf("expected configure to error") 584 } 585 586 expected := `The 'cloud' option is not supported with this version of Terraform Enterprise.` 587 if !strings.Contains(confDiags.Err().Error(), expected) { 588 t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) 589 } 590 } 591 592 func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) { 593 config := cty.ObjectVal(map[string]cty.Value{ 594 "hostname": cty.NullVal(cty.String), 595 "organization": cty.StringVal("hashicorp"), 596 "token": cty.NullVal(cty.String), 597 "workspaces": cty.ObjectVal(map[string]cty.Value{ 598 "name": cty.NullVal(cty.String), 599 "tags": cty.SetVal( 600 []cty.Value{ 601 cty.StringVal("billing"), 602 }, 603 ), 604 }), 605 }) 606 607 handlers := map[string]func(http.ResponseWriter, *http.Request){ 608 "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { 609 w.Header().Set("Content-Type", "application/json") 610 w.Header().Set("TFP-API-Version", "2.4") 611 }, 612 } 613 s := testServerWithHandlers(handlers) 614 615 b := New(testDisco(s)) 616 b.runningInAutomation = true 617 618 confDiags := b.Configure(config) 619 if confDiags.Err() == nil { 620 t.Fatalf("expected configure to error") 621 } 622 623 expected := `This version of Terraform Cloud/Enterprise does not support the state mechanism 624 attempting to be used by the platform. This should never happen.` 625 if !strings.Contains(confDiags.Err().Error(), expected) { 626 t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) 627 } 628 } 629 630 func TestCloud_setUnavailableTerraformVersion(t *testing.T) { 631 // go-tfe returns an error IRL if you try to set a Terraform version that's 632 // not available in your TFC instance. To test this, tfe_client_mock errors if 633 // you try to set any Terraform version for this specific workspace name. 634 workspaceName := "unavailable-terraform-version" 635 636 config := cty.ObjectVal(map[string]cty.Value{ 637 "hostname": cty.NullVal(cty.String), 638 "organization": cty.StringVal("hashicorp"), 639 "token": cty.NullVal(cty.String), 640 "workspaces": cty.ObjectVal(map[string]cty.Value{ 641 "name": cty.NullVal(cty.String), 642 "tags": cty.SetVal( 643 []cty.Value{ 644 cty.StringVal("sometag"), 645 }, 646 ), 647 }), 648 }) 649 650 b, bCleanup := testBackend(t, config) 651 defer bCleanup() 652 653 // Make sure the workspace doesn't exist yet -- otherwise, we can't test what 654 // happens when a workspace gets created. This is why we can't use "name" in 655 // the backend config above, btw: if you do, testBackend() creates the default 656 // workspace before we get a chance to do anything. 657 _, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) 658 if err != tfe.ErrResourceNotFound { 659 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) 660 } 661 662 _, err = b.StateMgr(workspaceName) 663 if err != nil { 664 t.Fatalf("expected no error from StateMgr, despite not being able to set remote Terraform version: %#v", err) 665 } 666 // Make sure the workspace was created: 667 workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) 668 if err != nil { 669 t.Fatalf("b.StateMgr() didn't actually create the desired workspace") 670 } 671 // Make sure our mocks still error as expected, using the same update function b.StateMgr() would call: 672 _, err = b.client.Workspaces.UpdateByID( 673 context.Background(), 674 workspace.ID, 675 tfe.WorkspaceUpdateOptions{TerraformVersion: tfe.String("1.1.0")}, 676 ) 677 if err == nil { 678 t.Fatalf("the mocks aren't emulating a nonexistent remote Terraform version correctly, so this test isn't trustworthy anymore") 679 } 680 } 681 682 func TestCloud_setConfigurationFields(t *testing.T) { 683 originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND") 684 685 cases := map[string]struct { 686 obj cty.Value 687 expectedHostname string 688 expectedOrganziation string 689 expectedWorkspaceName string 690 expectedWorkspaceTags []string 691 expectedForceLocal bool 692 setEnv func() 693 resetEnv func() 694 expectedErr string 695 }{ 696 "with hostname set": { 697 obj: cty.ObjectVal(map[string]cty.Value{ 698 "organization": cty.StringVal("hashicorp"), 699 "hostname": cty.StringVal("hashicorp.com"), 700 "workspaces": cty.ObjectVal(map[string]cty.Value{ 701 "name": cty.StringVal("prod"), 702 "tags": cty.NullVal(cty.Set(cty.String)), 703 }), 704 }), 705 expectedHostname: "hashicorp.com", 706 expectedOrganziation: "hashicorp", 707 }, 708 "with hostname not set, set to default hostname": { 709 obj: cty.ObjectVal(map[string]cty.Value{ 710 "organization": cty.StringVal("hashicorp"), 711 "hostname": cty.NullVal(cty.String), 712 "workspaces": cty.ObjectVal(map[string]cty.Value{ 713 "name": cty.StringVal("prod"), 714 "tags": cty.NullVal(cty.Set(cty.String)), 715 }), 716 }), 717 expectedHostname: defaultHostname, 718 expectedOrganziation: "hashicorp", 719 }, 720 "with workspace name set": { 721 obj: cty.ObjectVal(map[string]cty.Value{ 722 "organization": cty.StringVal("hashicorp"), 723 "hostname": cty.StringVal("hashicorp.com"), 724 "workspaces": cty.ObjectVal(map[string]cty.Value{ 725 "name": cty.StringVal("prod"), 726 "tags": cty.NullVal(cty.Set(cty.String)), 727 }), 728 }), 729 expectedHostname: "hashicorp.com", 730 expectedOrganziation: "hashicorp", 731 expectedWorkspaceName: "prod", 732 }, 733 "with workspace tags set": { 734 obj: cty.ObjectVal(map[string]cty.Value{ 735 "organization": cty.StringVal("hashicorp"), 736 "hostname": cty.StringVal("hashicorp.com"), 737 "workspaces": cty.ObjectVal(map[string]cty.Value{ 738 "name": cty.NullVal(cty.String), 739 "tags": cty.SetVal( 740 []cty.Value{ 741 cty.StringVal("billing"), 742 }, 743 ), 744 }), 745 }), 746 expectedHostname: "hashicorp.com", 747 expectedOrganziation: "hashicorp", 748 expectedWorkspaceTags: []string{"billing"}, 749 }, 750 "with force local set": { 751 obj: cty.ObjectVal(map[string]cty.Value{ 752 "organization": cty.StringVal("hashicorp"), 753 "hostname": cty.StringVal("hashicorp.com"), 754 "workspaces": cty.ObjectVal(map[string]cty.Value{ 755 "name": cty.NullVal(cty.String), 756 "tags": cty.NullVal(cty.Set(cty.String)), 757 }), 758 }), 759 expectedHostname: "hashicorp.com", 760 expectedOrganziation: "hashicorp", 761 setEnv: func() { 762 os.Setenv("TF_FORCE_LOCAL_BACKEND", "1") 763 }, 764 resetEnv: func() { 765 os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv) 766 }, 767 expectedForceLocal: true, 768 }, 769 } 770 771 for name, tc := range cases { 772 b := &Cloud{} 773 774 // if `setEnv` is set, then we expect `resetEnv` to also be set 775 if tc.setEnv != nil { 776 tc.setEnv() 777 defer tc.resetEnv() 778 } 779 780 errDiags := b.setConfigurationFields(tc.obj) 781 if errDiags.HasErrors() || tc.expectedErr != "" { 782 actualErr := errDiags.Err().Error() 783 if !strings.Contains(actualErr, tc.expectedErr) { 784 t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err()) 785 } 786 } 787 788 if tc.expectedHostname != "" && b.hostname != tc.expectedHostname { 789 t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname) 790 } 791 if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation { 792 t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation) 793 } 794 if tc.expectedWorkspaceName != "" && b.WorkspaceMapping.Name != tc.expectedWorkspaceName { 795 t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName) 796 } 797 if len(tc.expectedWorkspaceTags) > 0 { 798 presentSet := make(map[string]struct{}) 799 for _, tag := range b.WorkspaceMapping.Tags { 800 presentSet[tag] = struct{}{} 801 } 802 803 expectedSet := make(map[string]struct{}) 804 for _, tag := range tc.expectedWorkspaceTags { 805 expectedSet[tag] = struct{}{} 806 } 807 808 var missing []string 809 var unexpected []string 810 811 for _, expected := range tc.expectedWorkspaceTags { 812 if _, ok := presentSet[expected]; !ok { 813 missing = append(missing, expected) 814 } 815 } 816 817 for _, actual := range b.WorkspaceMapping.Tags { 818 if _, ok := expectedSet[actual]; !ok { 819 unexpected = append(unexpected, actual) 820 } 821 } 822 823 if len(missing) > 0 { 824 t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.WorkspaceMapping.Tags, missing) 825 } 826 827 if len(unexpected) > 0 { 828 t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.WorkspaceMapping.Tags, unexpected) 829 } 830 831 } 832 if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal { 833 t.Fatalf("%s: expected force local backend to be set ", name) 834 } 835 } 836 } 837 838 func TestCloud_localBackend(t *testing.T) { 839 b, bCleanup := testBackendWithName(t) 840 defer bCleanup() 841 842 local, ok := b.local.(*backendLocal.Local) 843 if !ok { 844 t.Fatalf("expected b.local to be \"*local.Local\", got: %T", b.local) 845 } 846 847 cloud, ok := local.Backend.(*Cloud) 848 if !ok { 849 t.Fatalf("expected local.Backend to be *cloud.Cloud, got: %T", cloud) 850 } 851 } 852 853 func TestCloud_addAndRemoveWorkspacesDefault(t *testing.T) { 854 b, bCleanup := testBackendWithName(t) 855 defer bCleanup() 856 857 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 858 t.Fatalf("expected no error, got %v", err) 859 } 860 861 if err := b.DeleteWorkspace(testBackendSingleWorkspaceName, true); err != backend.ErrWorkspacesNotSupported { 862 t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err) 863 } 864 } 865 866 func TestCloud_StateMgr_versionCheck(t *testing.T) { 867 b, bCleanup := testBackendWithName(t) 868 defer bCleanup() 869 870 // Some fixed versions for testing with. This logic is a simple string 871 // comparison, so we don't need many test cases. 872 v0135 := version.Must(version.NewSemver("0.13.5")) 873 v0140 := version.Must(version.NewSemver("0.14.0")) 874 875 // Save original local version state and restore afterwards 876 p := tfversion.Prerelease 877 v := tfversion.Version 878 s := tfversion.SemVer 879 defer func() { 880 tfversion.Prerelease = p 881 tfversion.Version = v 882 tfversion.SemVer = s 883 }() 884 885 // For this test, the local Terraform version is set to 0.14.0 886 tfversion.Prerelease = "" 887 tfversion.Version = v0140.String() 888 tfversion.SemVer = v0140 889 890 // Update the mock remote workspace Terraform version to match the local 891 // Terraform version 892 if _, err := b.client.Workspaces.Update( 893 context.Background(), 894 b.organization, 895 b.WorkspaceMapping.Name, 896 tfe.WorkspaceUpdateOptions{ 897 TerraformVersion: tfe.String(v0140.String()), 898 }, 899 ); err != nil { 900 t.Fatalf("error: %v", err) 901 } 902 903 // This should succeed 904 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 905 t.Fatalf("expected no error, got %v", err) 906 } 907 908 // Now change the remote workspace to a different Terraform version 909 if _, err := b.client.Workspaces.Update( 910 context.Background(), 911 b.organization, 912 b.WorkspaceMapping.Name, 913 tfe.WorkspaceUpdateOptions{ 914 TerraformVersion: tfe.String(v0135.String()), 915 }, 916 ); err != nil { 917 t.Fatalf("error: %v", err) 918 } 919 920 // This should fail 921 want := `Remote workspace Terraform version "0.13.5" does not match local Terraform version "0.14.0"` 922 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err.Error() != want { 923 t.Fatalf("wrong error\n got: %v\nwant: %v", err.Error(), want) 924 } 925 } 926 927 func TestCloud_StateMgr_versionCheckLatest(t *testing.T) { 928 b, bCleanup := testBackendWithName(t) 929 defer bCleanup() 930 931 v0140 := version.Must(version.NewSemver("0.14.0")) 932 933 // Save original local version state and restore afterwards 934 p := tfversion.Prerelease 935 v := tfversion.Version 936 s := tfversion.SemVer 937 defer func() { 938 tfversion.Prerelease = p 939 tfversion.Version = v 940 tfversion.SemVer = s 941 }() 942 943 // For this test, the local Terraform version is set to 0.14.0 944 tfversion.Prerelease = "" 945 tfversion.Version = v0140.String() 946 tfversion.SemVer = v0140 947 948 // Update the remote workspace to the pseudo-version "latest" 949 if _, err := b.client.Workspaces.Update( 950 context.Background(), 951 b.organization, 952 b.WorkspaceMapping.Name, 953 tfe.WorkspaceUpdateOptions{ 954 TerraformVersion: tfe.String("latest"), 955 }, 956 ); err != nil { 957 t.Fatalf("error: %v", err) 958 } 959 960 // This should succeed despite not being a string match 961 if _, err := b.StateMgr(testBackendSingleWorkspaceName); err != nil { 962 t.Fatalf("expected no error, got %v", err) 963 } 964 } 965 966 func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) { 967 testCases := []struct { 968 local string 969 remote string 970 executionMode string 971 wantErr bool 972 }{ 973 {"0.13.5", "0.13.5", "agent", false}, 974 {"0.14.0", "0.13.5", "remote", true}, 975 {"0.14.0", "0.13.5", "local", false}, 976 {"0.14.0", "0.14.1", "remote", false}, 977 {"0.14.0", "1.0.99", "remote", false}, 978 {"0.14.0", "1.1.0", "remote", false}, 979 {"0.14.0", "1.3.0", "remote", true}, 980 {"1.2.0", "1.2.99", "remote", false}, 981 {"1.2.0", "1.3.0", "remote", true}, 982 {"0.15.0", "latest", "remote", false}, 983 {"1.1.5", "~> 1.1.1", "remote", false}, 984 {"1.1.5", "> 1.1.0, < 1.3.0", "remote", false}, 985 {"1.1.5", "~> 1.0.1", "remote", true}, 986 // pre-release versions are comparable within their pre-release stage (dev, 987 // alpha, beta), but not comparable to different stages and not comparable 988 // to final releases. 989 {"1.1.0-beta1", "1.1.0-beta1", "remote", false}, 990 {"1.1.0-beta1", "~> 1.1.0-beta", "remote", false}, 991 {"1.1.0", "~> 1.1.0-beta", "remote", true}, 992 {"1.1.0-beta1", "~> 1.1.0-dev", "remote", true}, 993 } 994 for _, tc := range testCases { 995 t.Run(fmt.Sprintf("local %s, remote %s", tc.local, tc.remote), func(t *testing.T) { 996 b, bCleanup := testBackendWithName(t) 997 defer bCleanup() 998 999 local := version.Must(version.NewSemver(tc.local)) 1000 1001 // Save original local version state and restore afterwards 1002 p := tfversion.Prerelease 1003 v := tfversion.Version 1004 s := tfversion.SemVer 1005 defer func() { 1006 tfversion.Prerelease = p 1007 tfversion.Version = v 1008 tfversion.SemVer = s 1009 }() 1010 1011 // Override local version as specified 1012 tfversion.Prerelease = "" 1013 tfversion.Version = local.String() 1014 tfversion.SemVer = local 1015 1016 // Update the mock remote workspace Terraform version to the 1017 // specified remote version 1018 if _, err := b.client.Workspaces.Update( 1019 context.Background(), 1020 b.organization, 1021 b.WorkspaceMapping.Name, 1022 tfe.WorkspaceUpdateOptions{ 1023 ExecutionMode: &tc.executionMode, 1024 TerraformVersion: tfe.String(tc.remote), 1025 }, 1026 ); err != nil { 1027 t.Fatalf("error: %v", err) 1028 } 1029 1030 diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 1031 if tc.wantErr { 1032 if len(diags) != 1 { 1033 t.Fatal("expected diag, but none returned") 1034 } 1035 if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version") { 1036 t.Fatalf("unexpected error: %s", got) 1037 } 1038 } else { 1039 if len(diags) != 0 { 1040 t.Fatalf("unexpected diags: %s", diags.Err()) 1041 } 1042 } 1043 }) 1044 } 1045 } 1046 1047 func TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) { 1048 b, bCleanup := testBackendWithName(t) 1049 defer bCleanup() 1050 1051 // Attempting to check the version against a workspace which doesn't exist 1052 // should result in no errors 1053 diags := b.VerifyWorkspaceTerraformVersion("invalid-workspace") 1054 if len(diags) != 0 { 1055 t.Fatalf("unexpected error: %s", diags.Err()) 1056 } 1057 1058 // Use a special workspace ID to trigger a 500 error, which should result 1059 // in a failed check 1060 diags = b.VerifyWorkspaceTerraformVersion("network-error") 1061 if len(diags) != 1 { 1062 t.Fatal("expected diag, but none returned") 1063 } 1064 if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Workspace read failed") { 1065 t.Fatalf("unexpected error: %s", got) 1066 } 1067 1068 // Update the mock remote workspace Terraform version to an invalid version 1069 if _, err := b.client.Workspaces.Update( 1070 context.Background(), 1071 b.organization, 1072 b.WorkspaceMapping.Name, 1073 tfe.WorkspaceUpdateOptions{ 1074 TerraformVersion: tfe.String("1.0.cheetarah"), 1075 }, 1076 ); err != nil { 1077 t.Fatalf("error: %v", err) 1078 } 1079 diags = b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 1080 1081 if len(diags) != 1 { 1082 t.Fatal("expected diag, but none returned") 1083 } 1084 if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version: The remote workspace specified") { 1085 t.Fatalf("unexpected error: %s", got) 1086 } 1087 } 1088 1089 func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { 1090 b, bCleanup := testBackendWithName(t) 1091 defer bCleanup() 1092 1093 // If the ignore flag is set, the behaviour changes 1094 b.IgnoreVersionConflict() 1095 1096 // Different local & remote versions to cause an error 1097 local := version.Must(version.NewSemver("0.14.0")) 1098 remote := version.Must(version.NewSemver("0.13.5")) 1099 1100 // Save original local version state and restore afterwards 1101 p := tfversion.Prerelease 1102 v := tfversion.Version 1103 s := tfversion.SemVer 1104 defer func() { 1105 tfversion.Prerelease = p 1106 tfversion.Version = v 1107 tfversion.SemVer = s 1108 }() 1109 1110 // Override local version as specified 1111 tfversion.Prerelease = "" 1112 tfversion.Version = local.String() 1113 tfversion.SemVer = local 1114 1115 // Update the mock remote workspace Terraform version to the 1116 // specified remote version 1117 if _, err := b.client.Workspaces.Update( 1118 context.Background(), 1119 b.organization, 1120 b.WorkspaceMapping.Name, 1121 tfe.WorkspaceUpdateOptions{ 1122 TerraformVersion: tfe.String(remote.String()), 1123 }, 1124 ); err != nil { 1125 t.Fatalf("error: %v", err) 1126 } 1127 1128 diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) 1129 if len(diags) != 1 { 1130 t.Fatal("expected diag, but none returned") 1131 } 1132 1133 if got, want := diags[0].Severity(), tfdiags.Warning; got != want { 1134 t.Errorf("wrong severity: got %#v, want %#v", got, want) 1135 } 1136 if got, want := diags[0].Description().Summary, "Incompatible Terraform version"; got != want { 1137 t.Errorf("wrong summary: got %s, want %s", got, want) 1138 } 1139 wantDetail := "The local Terraform version (0.14.0) does not meet the version requirements for remote workspace hashicorp/app-prod (0.13.5)." 1140 if got := diags[0].Description().Detail; got != wantDetail { 1141 t.Errorf("wrong summary: got %s, want %s", got, wantDetail) 1142 } 1143 } 1144 1145 func TestClodBackend_DeleteWorkspace_SafeAndForce(t *testing.T) { 1146 b, bCleanup := testBackendWithTags(t) 1147 defer bCleanup() 1148 safeDeleteWorkspaceName := "safe-delete-workspace" 1149 forceDeleteWorkspaceName := "force-delete-workspace" 1150 1151 _, err := b.StateMgr(safeDeleteWorkspaceName) 1152 if err != nil { 1153 t.Fatalf("error: %s", err) 1154 } 1155 1156 _, err = b.StateMgr(forceDeleteWorkspaceName) 1157 if err != nil { 1158 t.Fatalf("error: %s", err) 1159 } 1160 1161 // sanity check that the mock now contains two workspaces 1162 wl, err := b.Workspaces() 1163 if err != nil { 1164 t.Fatalf("error fetching workspace names: %v", err) 1165 } 1166 if len(wl) != 2 { 1167 t.Fatalf("expected 2 workspaced but got %d", len(wl)) 1168 } 1169 1170 c := context.Background() 1171 safeDeleteWorkspace, err := b.client.Workspaces.Read(c, b.organization, safeDeleteWorkspaceName) 1172 if err != nil { 1173 t.Fatalf("error fetching workspace: %v", err) 1174 } 1175 1176 // Lock a workspace so that it should fail to be safe deleted 1177 _, err = b.client.Workspaces.Lock(context.Background(), safeDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")}) 1178 if err != nil { 1179 t.Fatalf("error locking workspace: %v", err) 1180 } 1181 err = b.DeleteWorkspace(safeDeleteWorkspaceName, false) 1182 if err == nil { 1183 t.Fatalf("workspace should have failed to safe delete") 1184 } 1185 1186 // unlock the workspace and confirm that safe-delete now works 1187 _, err = b.client.Workspaces.Unlock(context.Background(), safeDeleteWorkspace.ID) 1188 if err != nil { 1189 t.Fatalf("error unlocking workspace: %v", err) 1190 } 1191 err = b.DeleteWorkspace(safeDeleteWorkspaceName, false) 1192 if err != nil { 1193 t.Fatalf("error safe deleting workspace: %v", err) 1194 } 1195 1196 // lock a workspace and then confirm that force deleting it works 1197 forceDeleteWorkspace, err := b.client.Workspaces.Read(c, b.organization, forceDeleteWorkspaceName) 1198 if err != nil { 1199 t.Fatalf("error fetching workspace: %v", err) 1200 } 1201 _, err = b.client.Workspaces.Lock(context.Background(), forceDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")}) 1202 if err != nil { 1203 t.Fatalf("error locking workspace: %v", err) 1204 } 1205 err = b.DeleteWorkspace(forceDeleteWorkspaceName, true) 1206 if err != nil { 1207 t.Fatalf("error force deleting workspace: %v", err) 1208 } 1209 } 1210 1211 func TestClodBackend_DeleteWorkspace_DoesNotExist(t *testing.T) { 1212 b, bCleanup := testBackendWithTags(t) 1213 defer bCleanup() 1214 1215 err := b.DeleteWorkspace("non-existent-workspace", false) 1216 if err != nil { 1217 t.Fatalf("expected deleting a workspace which does not exist to succeed") 1218 } 1219 }