github.com/aspring/terraform@v0.8.2-0.20161216122603-6a8619a5db2e/command/push_test.go (about) 1 package command 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "compress/gzip" 7 "io" 8 "os" 9 "path/filepath" 10 "reflect" 11 "sort" 12 "testing" 13 14 atlas "github.com/hashicorp/atlas-go/v1" 15 "github.com/hashicorp/terraform/terraform" 16 "github.com/mitchellh/cli" 17 ) 18 19 func TestPush_good(t *testing.T) { 20 tmp, cwd := testCwd(t) 21 defer testFixCwd(t, tmp, cwd) 22 23 // Create remote state file, this should be pulled 24 conf, srv := testRemoteState(t, testState(), 200) 25 defer srv.Close() 26 27 // Persist local remote state 28 s := terraform.NewState() 29 s.Serial = 5 30 s.Remote = conf 31 testStateFileRemote(t, s) 32 33 // Path where the archive will be "uploaded" to 34 archivePath := testTempFile(t) 35 defer os.Remove(archivePath) 36 37 client := &mockPushClient{File: archivePath} 38 ui := new(cli.MockUi) 39 c := &PushCommand{ 40 Meta: Meta{ 41 ContextOpts: testCtxConfig(testProvider()), 42 Ui: ui, 43 }, 44 45 client: client, 46 } 47 48 args := []string{ 49 "-vcs=false", 50 testFixturePath("push"), 51 } 52 if code := c.Run(args); code != 0 { 53 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 54 } 55 56 actual := testArchiveStr(t, archivePath) 57 expected := []string{ 58 ".terraform/", 59 ".terraform/terraform.tfstate", 60 "main.tf", 61 } 62 if !reflect.DeepEqual(actual, expected) { 63 t.Fatalf("bad: %#v", actual) 64 } 65 66 variables := make(map[string]interface{}) 67 if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { 68 t.Fatalf("bad: %#v", client.UpsertOptions) 69 } 70 71 if client.UpsertOptions.Name != "foo" { 72 t.Fatalf("bad: %#v", client.UpsertOptions) 73 } 74 } 75 76 func TestPush_noUploadModules(t *testing.T) { 77 // Path where the archive will be "uploaded" to 78 archivePath := testTempFile(t) 79 defer os.Remove(archivePath) 80 81 client := &mockPushClient{File: archivePath} 82 ui := new(cli.MockUi) 83 c := &PushCommand{ 84 Meta: Meta{ 85 ContextOpts: testCtxConfig(testProvider()), 86 Ui: ui, 87 }, 88 89 client: client, 90 } 91 92 // Path of the test. We have to do some renaming to avoid our own 93 // VCS getting in the way. 94 path := testFixturePath("push-no-upload") 95 defer os.RemoveAll(filepath.Join(path, ".terraform")) 96 97 // Move into that directory 98 defer testChdir(t, path)() 99 100 // Do a "terraform get" 101 { 102 ui := new(cli.MockUi) 103 c := &GetCommand{ 104 Meta: Meta{ 105 ContextOpts: testCtxConfig(testProvider()), 106 Ui: ui, 107 }, 108 } 109 110 if code := c.Run([]string{}); code != 0 { 111 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 112 } 113 } 114 115 // Create remote state file, this should be pulled 116 conf, srv := testRemoteState(t, testState(), 200) 117 defer srv.Close() 118 119 // Persist local remote state 120 s := terraform.NewState() 121 s.Serial = 5 122 s.Remote = conf 123 defer os.Remove(testStateFileRemote(t, s)) 124 125 args := []string{ 126 "-vcs=false", 127 "-name=mitchellh/tf-test", 128 "-upload-modules=false", 129 path, 130 } 131 if code := c.Run(args); code != 0 { 132 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 133 } 134 135 // NOTE: The duplicates below are not ideal but are how things work 136 // currently due to how we manually add the files to the archive. This 137 // is definitely a "bug" we can fix in the future. 138 actual := testArchiveStr(t, archivePath) 139 expected := []string{ 140 ".terraform/", 141 ".terraform/", 142 ".terraform/terraform.tfstate", 143 ".terraform/terraform.tfstate", 144 "child/", 145 "child/main.tf", 146 "main.tf", 147 } 148 if !reflect.DeepEqual(actual, expected) { 149 t.Fatalf("bad: %#v", actual) 150 } 151 } 152 153 func TestPush_input(t *testing.T) { 154 tmp, cwd := testCwd(t) 155 defer testFixCwd(t, tmp, cwd) 156 157 // Create remote state file, this should be pulled 158 conf, srv := testRemoteState(t, testState(), 200) 159 defer srv.Close() 160 161 // Persist local remote state 162 s := terraform.NewState() 163 s.Serial = 5 164 s.Remote = conf 165 testStateFileRemote(t, s) 166 167 // Path where the archive will be "uploaded" to 168 archivePath := testTempFile(t) 169 defer os.Remove(archivePath) 170 171 client := &mockPushClient{File: archivePath} 172 ui := new(cli.MockUi) 173 c := &PushCommand{ 174 Meta: Meta{ 175 ContextOpts: testCtxConfig(testProvider()), 176 Ui: ui, 177 }, 178 179 client: client, 180 } 181 182 // Disable test mode so input would be asked and setup the 183 // input reader/writers. 184 test = false 185 defer func() { test = true }() 186 defaultInputReader = bytes.NewBufferString("foo\n") 187 defaultInputWriter = new(bytes.Buffer) 188 189 args := []string{ 190 "-vcs=false", 191 testFixturePath("push-input"), 192 } 193 if code := c.Run(args); code != 0 { 194 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 195 } 196 197 variables := map[string]interface{}{ 198 "foo": "foo", 199 } 200 201 if !reflect.DeepEqual(client.UpsertOptions.Variables, variables) { 202 t.Fatalf("bad: %#v", client.UpsertOptions.Variables) 203 } 204 } 205 206 // We want a variable from atlas to fill a missing variable locally 207 func TestPush_inputPartial(t *testing.T) { 208 tmp, cwd := testCwd(t) 209 defer testFixCwd(t, tmp, cwd) 210 211 // Create remote state file, this should be pulled 212 conf, srv := testRemoteState(t, testState(), 200) 213 defer srv.Close() 214 215 // Persist local remote state 216 s := terraform.NewState() 217 s.Serial = 5 218 s.Remote = conf 219 testStateFileRemote(t, s) 220 221 // Path where the archive will be "uploaded" to 222 archivePath := testTempFile(t) 223 defer os.Remove(archivePath) 224 225 client := &mockPushClient{ 226 File: archivePath, 227 GetResult: map[string]atlas.TFVar{ 228 "foo": atlas.TFVar{Key: "foo", Value: "bar"}, 229 }, 230 } 231 ui := new(cli.MockUi) 232 c := &PushCommand{ 233 Meta: Meta{ 234 ContextOpts: testCtxConfig(testProvider()), 235 Ui: ui, 236 }, 237 238 client: client, 239 } 240 241 // Disable test mode so input would be asked and setup the 242 // input reader/writers. 243 test = false 244 defer func() { test = true }() 245 defaultInputReader = bytes.NewBufferString("foo\n") 246 defaultInputWriter = new(bytes.Buffer) 247 248 args := []string{ 249 "-vcs=false", 250 testFixturePath("push-input-partial"), 251 } 252 if code := c.Run(args); code != 0 { 253 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 254 } 255 256 expectedTFVars := []atlas.TFVar{ 257 {Key: "bar", Value: "foo"}, 258 {Key: "foo", Value: "bar"}, 259 } 260 if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) { 261 t.Logf("expected: %#v", expectedTFVars) 262 t.Fatalf("got: %#v", client.UpsertOptions.TFVars) 263 } 264 } 265 266 // This tests that the push command will override Atlas variables 267 // if requested. 268 func TestPush_localOverride(t *testing.T) { 269 // Disable test mode so input would be asked and setup the 270 // input reader/writers. 271 test = false 272 defer func() { test = true }() 273 defaultInputReader = bytes.NewBufferString("nope\n") 274 defaultInputWriter = new(bytes.Buffer) 275 276 tmp, cwd := testCwd(t) 277 defer testFixCwd(t, tmp, cwd) 278 279 // Create remote state file, this should be pulled 280 conf, srv := testRemoteState(t, testState(), 200) 281 defer srv.Close() 282 283 // Persist local remote state 284 s := terraform.NewState() 285 s.Serial = 5 286 s.Remote = conf 287 testStateFileRemote(t, s) 288 289 // Path where the archive will be "uploaded" to 290 archivePath := testTempFile(t) 291 defer os.Remove(archivePath) 292 293 client := &mockPushClient{File: archivePath} 294 // Provided vars should override existing ones 295 client.GetResult = map[string]atlas.TFVar{ 296 "foo": atlas.TFVar{ 297 Key: "foo", 298 Value: "old", 299 }, 300 } 301 ui := new(cli.MockUi) 302 c := &PushCommand{ 303 Meta: Meta{ 304 ContextOpts: testCtxConfig(testProvider()), 305 Ui: ui, 306 }, 307 308 client: client, 309 } 310 311 path := testFixturePath("push-tfvars") 312 args := []string{ 313 "-var-file", path + "/terraform.tfvars", 314 "-vcs=false", 315 "-overwrite=foo", 316 path, 317 } 318 if code := c.Run(args); code != 0 { 319 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 320 } 321 322 actual := testArchiveStr(t, archivePath) 323 expected := []string{ 324 ".terraform/", 325 ".terraform/terraform.tfstate", 326 "main.tf", 327 "terraform.tfvars", 328 } 329 if !reflect.DeepEqual(actual, expected) { 330 t.Fatalf("bad: %#v", actual) 331 } 332 333 if client.UpsertOptions.Name != "foo" { 334 t.Fatalf("bad: %#v", client.UpsertOptions) 335 } 336 337 expectedTFVars := pushTFVars() 338 339 if !reflect.DeepEqual(client.UpsertOptions.TFVars, expectedTFVars) { 340 t.Logf("expected: %#v", expectedTFVars) 341 t.Fatalf("got: %#v", client.UpsertOptions.TFVars) 342 } 343 } 344 345 // This tests that the push command will override Atlas variables 346 // even if we don't have it defined locally 347 func TestPush_remoteOverride(t *testing.T) { 348 // Disable test mode so input would be asked and setup the 349 // input reader/writers. 350 test = false 351 defer func() { test = true }() 352 defaultInputReader = bytes.NewBufferString("nope\n") 353 defaultInputWriter = new(bytes.Buffer) 354 355 tmp, cwd := testCwd(t) 356 defer testFixCwd(t, tmp, cwd) 357 358 // Create remote state file, this should be pulled 359 conf, srv := testRemoteState(t, testState(), 200) 360 defer srv.Close() 361 362 // Persist local remote state 363 s := terraform.NewState() 364 s.Serial = 5 365 s.Remote = conf 366 testStateFileRemote(t, s) 367 368 // Path where the archive will be "uploaded" to 369 archivePath := testTempFile(t) 370 defer os.Remove(archivePath) 371 372 client := &mockPushClient{File: archivePath} 373 // Provided vars should override existing ones 374 client.GetResult = map[string]atlas.TFVar{ 375 "remote": atlas.TFVar{ 376 Key: "remote", 377 Value: "old", 378 }, 379 } 380 ui := new(cli.MockUi) 381 c := &PushCommand{ 382 Meta: Meta{ 383 ContextOpts: testCtxConfig(testProvider()), 384 Ui: ui, 385 }, 386 387 client: client, 388 } 389 390 path := testFixturePath("push-tfvars") 391 args := []string{ 392 "-var-file", path + "/terraform.tfvars", 393 "-vcs=false", 394 "-overwrite=remote", 395 "-var", 396 "remote=new", 397 path, 398 } 399 400 if code := c.Run(args); code != 0 { 401 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 402 } 403 404 actual := testArchiveStr(t, archivePath) 405 expected := []string{ 406 ".terraform/", 407 ".terraform/terraform.tfstate", 408 "main.tf", 409 "terraform.tfvars", 410 } 411 if !reflect.DeepEqual(actual, expected) { 412 t.Fatalf("bad: %#v", actual) 413 } 414 415 if client.UpsertOptions.Name != "foo" { 416 t.Fatalf("bad: %#v", client.UpsertOptions) 417 } 418 419 found := false 420 // find the "remote" var and make sure we're going to set it 421 for _, tfVar := range client.UpsertOptions.TFVars { 422 if tfVar.Key == "remote" { 423 found = true 424 if tfVar.Value != "new" { 425 t.Log("'remote' variable should be set to 'new'") 426 t.Fatalf("sending instead: %#v", tfVar) 427 } 428 } 429 } 430 431 if !found { 432 t.Fatal("'remote' variable not being sent to atlas") 433 } 434 } 435 436 // This tests that the push command prefers Atlas variables over 437 // local ones. 438 func TestPush_preferAtlas(t *testing.T) { 439 // Disable test mode so input would be asked and setup the 440 // input reader/writers. 441 test = false 442 defer func() { test = true }() 443 defaultInputReader = bytes.NewBufferString("nope\n") 444 defaultInputWriter = new(bytes.Buffer) 445 446 tmp, cwd := testCwd(t) 447 defer testFixCwd(t, tmp, cwd) 448 449 // Create remote state file, this should be pulled 450 conf, srv := testRemoteState(t, testState(), 200) 451 defer srv.Close() 452 453 // Persist local remote state 454 s := terraform.NewState() 455 s.Serial = 5 456 s.Remote = conf 457 testStateFileRemote(t, s) 458 459 // Path where the archive will be "uploaded" to 460 archivePath := testTempFile(t) 461 defer os.Remove(archivePath) 462 463 client := &mockPushClient{File: archivePath} 464 // Provided vars should override existing ones 465 client.GetResult = map[string]atlas.TFVar{ 466 "foo": atlas.TFVar{ 467 Key: "foo", 468 Value: "old", 469 }, 470 } 471 ui := new(cli.MockUi) 472 c := &PushCommand{ 473 Meta: Meta{ 474 ContextOpts: testCtxConfig(testProvider()), 475 Ui: ui, 476 }, 477 478 client: client, 479 } 480 481 path := testFixturePath("push-tfvars") 482 args := []string{ 483 "-var-file", path + "/terraform.tfvars", 484 "-vcs=false", 485 path, 486 } 487 if code := c.Run(args); code != 0 { 488 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 489 } 490 491 actual := testArchiveStr(t, archivePath) 492 expected := []string{ 493 ".terraform/", 494 ".terraform/terraform.tfstate", 495 "main.tf", 496 "terraform.tfvars", 497 } 498 if !reflect.DeepEqual(actual, expected) { 499 t.Fatalf("bad: %#v", actual) 500 } 501 502 if client.UpsertOptions.Name != "foo" { 503 t.Fatalf("bad: %#v", client.UpsertOptions) 504 } 505 506 // change the expected response to match our change 507 expectedTFVars := pushTFVars() 508 for i, v := range expectedTFVars { 509 if v.Key == "foo" { 510 expectedTFVars[i] = atlas.TFVar{Key: "foo", Value: "old"} 511 } 512 } 513 514 if !reflect.DeepEqual(expectedTFVars, client.UpsertOptions.TFVars) { 515 t.Logf("expected: %#v", expectedTFVars) 516 t.Fatalf("got: %#v", client.UpsertOptions.TFVars) 517 } 518 } 519 520 // This tests that the push command will send the variables in tfvars 521 func TestPush_tfvars(t *testing.T) { 522 // Disable test mode so input would be asked and setup the 523 // input reader/writers. 524 test = false 525 defer func() { test = true }() 526 defaultInputReader = bytes.NewBufferString("nope\n") 527 defaultInputWriter = new(bytes.Buffer) 528 529 tmp, cwd := testCwd(t) 530 defer testFixCwd(t, tmp, cwd) 531 532 // Create remote state file, this should be pulled 533 conf, srv := testRemoteState(t, testState(), 200) 534 defer srv.Close() 535 536 // Persist local remote state 537 s := terraform.NewState() 538 s.Serial = 5 539 s.Remote = conf 540 testStateFileRemote(t, s) 541 542 // Path where the archive will be "uploaded" to 543 archivePath := testTempFile(t) 544 defer os.Remove(archivePath) 545 546 client := &mockPushClient{File: archivePath} 547 ui := new(cli.MockUi) 548 c := &PushCommand{ 549 Meta: Meta{ 550 ContextOpts: testCtxConfig(testProvider()), 551 Ui: ui, 552 }, 553 554 client: client, 555 } 556 557 path := testFixturePath("push-tfvars") 558 args := []string{ 559 "-var-file", path + "/terraform.tfvars", 560 "-vcs=false", 561 "-var", 562 "bar=[1,2]", 563 path, 564 } 565 if code := c.Run(args); code != 0 { 566 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 567 } 568 569 actual := testArchiveStr(t, archivePath) 570 expected := []string{ 571 ".terraform/", 572 ".terraform/terraform.tfstate", 573 "main.tf", 574 "terraform.tfvars", 575 } 576 if !reflect.DeepEqual(actual, expected) { 577 t.Fatalf("bad: %#v", actual) 578 } 579 580 if client.UpsertOptions.Name != "foo" { 581 t.Fatalf("bad: %#v", client.UpsertOptions) 582 } 583 584 //now check TFVars 585 tfvars := pushTFVars() 586 // update bar to match cli value 587 for i, v := range tfvars { 588 if v.Key == "bar" { 589 tfvars[i].Value = "[1, 2]" 590 tfvars[i].IsHCL = true 591 } 592 } 593 594 for i, expected := range tfvars { 595 got := client.UpsertOptions.TFVars[i] 596 if got != expected { 597 t.Logf("%2d expected: %#v", i, expected) 598 t.Fatalf(" got: %#v", got) 599 } 600 } 601 } 602 603 func TestPush_name(t *testing.T) { 604 tmp, cwd := testCwd(t) 605 defer testFixCwd(t, tmp, cwd) 606 607 // Create remote state file, this should be pulled 608 conf, srv := testRemoteState(t, testState(), 200) 609 defer srv.Close() 610 611 // Persist local remote state 612 s := terraform.NewState() 613 s.Serial = 5 614 s.Remote = conf 615 testStateFileRemote(t, s) 616 617 // Path where the archive will be "uploaded" to 618 archivePath := testTempFile(t) 619 defer os.Remove(archivePath) 620 621 client := &mockPushClient{File: archivePath} 622 ui := new(cli.MockUi) 623 c := &PushCommand{ 624 Meta: Meta{ 625 ContextOpts: testCtxConfig(testProvider()), 626 Ui: ui, 627 }, 628 629 client: client, 630 } 631 632 args := []string{ 633 "-name", "bar", 634 "-vcs=false", 635 testFixturePath("push"), 636 } 637 if code := c.Run(args); code != 0 { 638 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 639 } 640 641 if client.UpsertOptions.Name != "bar" { 642 t.Fatalf("bad: %#v", client.UpsertOptions) 643 } 644 } 645 646 func TestPush_noState(t *testing.T) { 647 tmp, cwd := testCwd(t) 648 defer testFixCwd(t, tmp, cwd) 649 650 ui := new(cli.MockUi) 651 c := &PushCommand{ 652 Meta: Meta{ 653 ContextOpts: testCtxConfig(testProvider()), 654 Ui: ui, 655 }, 656 } 657 658 args := []string{} 659 if code := c.Run(args); code != 1 { 660 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 661 } 662 } 663 664 func TestPush_noRemoteState(t *testing.T) { 665 state := &terraform.State{ 666 Modules: []*terraform.ModuleState{ 667 &terraform.ModuleState{ 668 Path: []string{"root"}, 669 Resources: map[string]*terraform.ResourceState{ 670 "test_instance.foo": &terraform.ResourceState{ 671 Type: "test_instance", 672 Primary: &terraform.InstanceState{ 673 ID: "bar", 674 }, 675 }, 676 }, 677 }, 678 }, 679 } 680 statePath := testStateFile(t, state) 681 682 ui := new(cli.MockUi) 683 c := &PushCommand{ 684 Meta: Meta{ 685 Ui: ui, 686 }, 687 } 688 689 args := []string{ 690 "-state", statePath, 691 } 692 if code := c.Run(args); code != 1 { 693 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 694 } 695 } 696 697 func TestPush_plan(t *testing.T) { 698 tmp, cwd := testCwd(t) 699 defer testFixCwd(t, tmp, cwd) 700 701 // Create remote state file, this should be pulled 702 conf, srv := testRemoteState(t, testState(), 200) 703 defer srv.Close() 704 705 // Persist local remote state 706 s := terraform.NewState() 707 s.Serial = 5 708 s.Remote = conf 709 testStateFileRemote(t, s) 710 711 // Create a plan 712 planPath := testPlanFile(t, &terraform.Plan{ 713 Module: testModule(t, "apply"), 714 }) 715 716 ui := new(cli.MockUi) 717 c := &PushCommand{ 718 Meta: Meta{ 719 ContextOpts: testCtxConfig(testProvider()), 720 Ui: ui, 721 }, 722 } 723 724 args := []string{planPath} 725 if code := c.Run(args); code != 1 { 726 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 727 } 728 } 729 730 func testArchiveStr(t *testing.T, path string) []string { 731 f, err := os.Open(path) 732 if err != nil { 733 t.Fatalf("err: %s", err) 734 } 735 defer f.Close() 736 737 // Ungzip 738 gzipR, err := gzip.NewReader(f) 739 if err != nil { 740 t.Fatalf("err: %s", err) 741 } 742 743 // Accumulator 744 result := make([]string, 0, 10) 745 746 // Untar 747 tarR := tar.NewReader(gzipR) 748 for { 749 header, err := tarR.Next() 750 if err == io.EOF { 751 break 752 } 753 if err != nil { 754 t.Fatalf("err: %s", err) 755 } 756 757 result = append(result, header.Name) 758 } 759 760 sort.Strings(result) 761 return result 762 } 763 764 func pushTFVars() []atlas.TFVar { 765 return []atlas.TFVar{ 766 {Key: "bar", Value: "foo", IsHCL: false}, 767 {Key: "baz", Value: `{ 768 A = "a" 769 }`, IsHCL: true}, 770 {Key: "fob", Value: `["a", "quotes \"in\" quotes"]`, IsHCL: true}, 771 {Key: "foo", Value: "bar", IsHCL: false}, 772 } 773 } 774 775 // the structure returned from the push-tfvars test fixture 776 func pushTFVarsMap() map[string]atlas.TFVar { 777 vars := make(map[string]atlas.TFVar) 778 for _, v := range pushTFVars() { 779 vars[v.Key] = v 780 } 781 return vars 782 }