github.com/salme4/terraform@v0.11.12-beta1/command/command_test.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "encoding/base64" 7 "encoding/json" 8 "flag" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "log" 13 "net/http" 14 "net/http/httptest" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "strings" 19 "syscall" 20 "testing" 21 22 backendInit "github.com/hashicorp/terraform/backend/init" 23 "github.com/hashicorp/terraform/config/module" 24 "github.com/hashicorp/terraform/helper/logging" 25 "github.com/hashicorp/terraform/terraform" 26 ) 27 28 // This is the directory where our test fixtures are. 29 var fixtureDir = "./test-fixtures" 30 31 // a top level temp directory which will be cleaned after all tests 32 var testingDir string 33 34 func init() { 35 test = true 36 37 // Initialize the backends 38 backendInit.Init(nil) 39 40 // Expand the fixture dir on init because we change the working 41 // directory in some tests. 42 var err error 43 fixtureDir, err = filepath.Abs(fixtureDir) 44 if err != nil { 45 panic(err) 46 } 47 48 testingDir, err = ioutil.TempDir(testingDir, "tf") 49 if err != nil { 50 panic(err) 51 } 52 } 53 54 func TestMain(m *testing.M) { 55 defer os.RemoveAll(testingDir) 56 57 flag.Parse() 58 if testing.Verbose() { 59 // if we're verbose, use the logging requested by TF_LOG 60 logging.SetOutput() 61 } else { 62 // otherwise silence all logs 63 log.SetOutput(ioutil.Discard) 64 } 65 66 os.Exit(m.Run()) 67 } 68 69 func tempDir(t *testing.T) string { 70 t.Helper() 71 72 dir, err := ioutil.TempDir(testingDir, "tf") 73 if err != nil { 74 t.Fatalf("err: %s", err) 75 } 76 if err := os.RemoveAll(dir); err != nil { 77 t.Fatalf("err: %s", err) 78 } 79 80 return dir 81 } 82 83 func testFixturePath(name string) string { 84 return filepath.Join(fixtureDir, name) 85 } 86 87 func metaOverridesForProvider(p terraform.ResourceProvider) *testingOverrides { 88 return &testingOverrides{ 89 ProviderResolver: terraform.ResourceProviderResolverFixed( 90 map[string]terraform.ResourceProviderFactory{ 91 "test": func() (terraform.ResourceProvider, error) { 92 return p, nil 93 }, 94 }, 95 ), 96 } 97 } 98 99 func metaOverridesForProviderAndProvisioner(p terraform.ResourceProvider, pr terraform.ResourceProvisioner) *testingOverrides { 100 return &testingOverrides{ 101 ProviderResolver: terraform.ResourceProviderResolverFixed( 102 map[string]terraform.ResourceProviderFactory{ 103 "test": func() (terraform.ResourceProvider, error) { 104 return p, nil 105 }, 106 }, 107 ), 108 Provisioners: map[string]terraform.ResourceProvisionerFactory{ 109 "shell": func() (terraform.ResourceProvisioner, error) { 110 return pr, nil 111 }, 112 }, 113 } 114 } 115 116 func testModule(t *testing.T, name string) *module.Tree { 117 t.Helper() 118 119 mod, err := module.NewTreeModule("", filepath.Join(fixtureDir, name)) 120 if err != nil { 121 t.Fatalf("err: %s", err) 122 } 123 124 s := module.NewStorage(tempDir(t), nil) 125 s.Mode = module.GetModeGet 126 if err := mod.Load(s); err != nil { 127 t.Fatalf("err: %s", err) 128 } 129 130 return mod 131 } 132 133 // testPlan returns a non-nil noop plan. 134 func testPlan(t *testing.T) *terraform.Plan { 135 t.Helper() 136 137 state := terraform.NewState() 138 state.RootModule().Outputs["foo"] = &terraform.OutputState{ 139 Type: "string", 140 Value: "foo", 141 } 142 143 return &terraform.Plan{ 144 Module: testModule(t, "apply"), 145 State: state, 146 } 147 } 148 149 func testPlanFile(t *testing.T, plan *terraform.Plan) string { 150 t.Helper() 151 152 path := testTempFile(t) 153 154 f, err := os.Create(path) 155 if err != nil { 156 t.Fatalf("err: %s", err) 157 } 158 defer f.Close() 159 160 if err := terraform.WritePlan(plan, f); err != nil { 161 t.Fatalf("err: %s", err) 162 } 163 164 return path 165 } 166 167 func testReadPlan(t *testing.T, path string) *terraform.Plan { 168 t.Helper() 169 170 f, err := os.Open(path) 171 if err != nil { 172 t.Fatalf("err: %s", err) 173 } 174 defer f.Close() 175 176 p, err := terraform.ReadPlan(f) 177 if err != nil { 178 t.Fatalf("err: %s", err) 179 } 180 181 return p 182 } 183 184 // testState returns a test State structure that we use for a lot of tests. 185 func testState() *terraform.State { 186 state := &terraform.State{ 187 Modules: []*terraform.ModuleState{ 188 &terraform.ModuleState{ 189 Path: []string{"root"}, 190 Resources: map[string]*terraform.ResourceState{ 191 "test_instance.foo": &terraform.ResourceState{ 192 Type: "test_instance", 193 Primary: &terraform.InstanceState{ 194 ID: "bar", 195 }, 196 }, 197 }, 198 Outputs: map[string]*terraform.OutputState{}, 199 }, 200 }, 201 } 202 state.Init() 203 return state 204 } 205 206 func testStateFile(t *testing.T, s *terraform.State) string { 207 t.Helper() 208 209 path := testTempFile(t) 210 211 f, err := os.Create(path) 212 if err != nil { 213 t.Fatalf("err: %s", err) 214 } 215 defer f.Close() 216 217 if err := terraform.WriteState(s, f); err != nil { 218 t.Fatalf("err: %s", err) 219 } 220 221 return path 222 } 223 224 // testStateFileDefault writes the state out to the default statefile 225 // in the cwd. Use `testCwd` to change into a temp cwd. 226 func testStateFileDefault(t *testing.T, s *terraform.State) string { 227 t.Helper() 228 229 f, err := os.Create(DefaultStateFilename) 230 if err != nil { 231 t.Fatalf("err: %s", err) 232 } 233 defer f.Close() 234 235 if err := terraform.WriteState(s, f); err != nil { 236 t.Fatalf("err: %s", err) 237 } 238 239 return DefaultStateFilename 240 } 241 242 // testStateFileRemote writes the state out to the remote statefile 243 // in the cwd. Use `testCwd` to change into a temp cwd. 244 func testStateFileRemote(t *testing.T, s *terraform.State) string { 245 t.Helper() 246 247 path := filepath.Join(DefaultDataDir, DefaultStateFilename) 248 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 249 t.Fatalf("err: %s", err) 250 } 251 252 f, err := os.Create(path) 253 if err != nil { 254 t.Fatalf("err: %s", err) 255 } 256 defer f.Close() 257 258 if err := terraform.WriteState(s, f); err != nil { 259 t.Fatalf("err: %s", err) 260 } 261 262 return path 263 } 264 265 // testStateRead reads the state from a file 266 func testStateRead(t *testing.T, path string) *terraform.State { 267 t.Helper() 268 269 f, err := os.Open(path) 270 if err != nil { 271 t.Fatalf("err: %s", err) 272 } 273 defer f.Close() 274 275 newState, err := terraform.ReadState(f) 276 if err != nil { 277 t.Fatalf("err: %s", err) 278 } 279 280 return newState 281 } 282 283 // testStateOutput tests that the state at the given path contains 284 // the expected state string. 285 func testStateOutput(t *testing.T, path string, expected string) { 286 t.Helper() 287 288 newState := testStateRead(t, path) 289 actual := strings.TrimSpace(newState.String()) 290 expected = strings.TrimSpace(expected) 291 if actual != expected { 292 t.Fatalf("expected:\n%s\nactual:\n%s", expected, actual) 293 } 294 } 295 296 func testProvider() *terraform.MockResourceProvider { 297 p := new(terraform.MockResourceProvider) 298 p.DiffReturn = &terraform.InstanceDiff{} 299 p.RefreshFn = func( 300 info *terraform.InstanceInfo, 301 s *terraform.InstanceState) (*terraform.InstanceState, error) { 302 return s, nil 303 } 304 p.ResourcesReturn = []terraform.ResourceType{ 305 terraform.ResourceType{ 306 Name: "test_instance", 307 }, 308 } 309 310 return p 311 } 312 313 func testTempFile(t *testing.T) string { 314 t.Helper() 315 316 return filepath.Join(testTempDir(t), "state.tfstate") 317 } 318 319 func testTempDir(t *testing.T) string { 320 t.Helper() 321 322 d, err := ioutil.TempDir(testingDir, "tf") 323 if err != nil { 324 t.Fatalf("err: %s", err) 325 } 326 327 return d 328 } 329 330 // testRename renames the path to new and returns a function to defer to 331 // revert the rename. 332 func testRename(t *testing.T, base, path, new string) func() { 333 t.Helper() 334 335 if base != "" { 336 path = filepath.Join(base, path) 337 new = filepath.Join(base, new) 338 } 339 340 if err := os.Rename(path, new); err != nil { 341 t.Fatalf("err: %s", err) 342 } 343 344 return func() { 345 // Just re-rename and ignore the return value 346 testRename(t, "", new, path) 347 } 348 } 349 350 // testChdir changes the directory and returns a function to defer to 351 // revert the old cwd. 352 func testChdir(t *testing.T, new string) func() { 353 t.Helper() 354 355 old, err := os.Getwd() 356 if err != nil { 357 t.Fatalf("err: %s", err) 358 } 359 360 if err := os.Chdir(new); err != nil { 361 t.Fatalf("err: %v", err) 362 } 363 364 return func() { 365 // Re-run the function ignoring the defer result 366 testChdir(t, old) 367 } 368 } 369 370 // testCwd is used to change the current working directory 371 // into a test directory that should be remoted after 372 func testCwd(t *testing.T) (string, string) { 373 t.Helper() 374 375 tmp, err := ioutil.TempDir(testingDir, "tf") 376 if err != nil { 377 t.Fatalf("err: %v", err) 378 } 379 380 cwd, err := os.Getwd() 381 if err != nil { 382 t.Fatalf("err: %v", err) 383 } 384 385 if err := os.Chdir(tmp); err != nil { 386 t.Fatalf("err: %v", err) 387 } 388 389 return tmp, cwd 390 } 391 392 // testFixCwd is used to as a defer to testDir 393 func testFixCwd(t *testing.T, tmp, cwd string) { 394 t.Helper() 395 396 if err := os.Chdir(cwd); err != nil { 397 t.Fatalf("err: %v", err) 398 } 399 400 if err := os.RemoveAll(tmp); err != nil { 401 t.Fatalf("err: %v", err) 402 } 403 } 404 405 // testStdinPipe changes os.Stdin to be a pipe that sends the data from 406 // the reader before closing the pipe. 407 // 408 // The returned function should be deferred to properly clean up and restore 409 // the original stdin. 410 func testStdinPipe(t *testing.T, src io.Reader) func() { 411 t.Helper() 412 413 r, w, err := os.Pipe() 414 if err != nil { 415 t.Fatalf("err: %s", err) 416 } 417 418 // Modify stdin to point to our new pipe 419 old := os.Stdin 420 os.Stdin = r 421 422 // Copy the data from the reader to the pipe 423 go func() { 424 defer w.Close() 425 io.Copy(w, src) 426 }() 427 428 return func() { 429 // Close our read end 430 r.Close() 431 432 // Reset stdin 433 os.Stdin = old 434 } 435 } 436 437 // Modify os.Stdout to write to the given buffer. Note that this is generally 438 // not useful since the commands are configured to write to a cli.Ui, not 439 // Stdout directly. Commands like `console` though use the raw stdout. 440 func testStdoutCapture(t *testing.T, dst io.Writer) func() { 441 t.Helper() 442 443 r, w, err := os.Pipe() 444 if err != nil { 445 t.Fatalf("err: %s", err) 446 } 447 448 // Modify stdout 449 old := os.Stdout 450 os.Stdout = w 451 452 // Copy 453 doneCh := make(chan struct{}) 454 go func() { 455 defer close(doneCh) 456 defer r.Close() 457 io.Copy(dst, r) 458 }() 459 460 return func() { 461 // Close the writer end of the pipe 462 w.Sync() 463 w.Close() 464 465 // Reset stdout 466 os.Stdout = old 467 468 // Wait for the data copy to complete to avoid a race reading data 469 <-doneCh 470 } 471 } 472 473 // testInteractiveInput configures tests so that the answers given are sent 474 // in order to interactive prompts. The returned function must be called 475 // in a defer to clean up. 476 func testInteractiveInput(t *testing.T, answers []string) func() { 477 t.Helper() 478 479 // Disable test mode so input is called 480 test = false 481 482 // Setup reader/writers 483 testInputResponse = answers 484 defaultInputReader = bytes.NewBufferString("") 485 defaultInputWriter = new(bytes.Buffer) 486 487 // Return the cleanup 488 return func() { 489 test = true 490 testInputResponse = nil 491 } 492 } 493 494 // testInputMap configures tests so that the given answers are returned 495 // for calls to Input when the right question is asked. The key is the 496 // question "Id" that is used. 497 func testInputMap(t *testing.T, answers map[string]string) func() { 498 t.Helper() 499 500 // Disable test mode so input is called 501 test = false 502 503 // Setup reader/writers 504 defaultInputReader = bytes.NewBufferString("") 505 defaultInputWriter = new(bytes.Buffer) 506 507 // Setup answers 508 testInputResponse = nil 509 testInputResponseMap = answers 510 511 // Return the cleanup 512 return func() { 513 test = true 514 testInputResponseMap = nil 515 } 516 } 517 518 // testBackendState is used to make a test HTTP server to test a configured 519 // backend. This returns the complete state that can be saved. Use 520 // `testStateFileRemote` to write the returned state. 521 func testBackendState(t *testing.T, s *terraform.State, c int) (*terraform.State, *httptest.Server) { 522 t.Helper() 523 524 var b64md5 string 525 buf := bytes.NewBuffer(nil) 526 527 cb := func(resp http.ResponseWriter, req *http.Request) { 528 if req.Method == "PUT" { 529 resp.WriteHeader(c) 530 return 531 } 532 if s == nil { 533 resp.WriteHeader(404) 534 return 535 } 536 537 resp.Header().Set("Content-MD5", b64md5) 538 resp.Write(buf.Bytes()) 539 } 540 541 // If a state was given, make sure we calculate the proper b64md5 542 if s != nil { 543 enc := json.NewEncoder(buf) 544 if err := enc.Encode(s); err != nil { 545 t.Fatalf("err: %v", err) 546 } 547 md5 := md5.Sum(buf.Bytes()) 548 b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) 549 } 550 551 srv := httptest.NewServer(http.HandlerFunc(cb)) 552 553 state := terraform.NewState() 554 state.Backend = &terraform.BackendState{ 555 Type: "http", 556 Config: map[string]interface{}{"address": srv.URL}, 557 Hash: 2529831861221416334, 558 } 559 560 return state, srv 561 } 562 563 // testRemoteState is used to make a test HTTP server to return a given 564 // state file that can be used for testing legacy remote state. 565 func testRemoteState(t *testing.T, s *terraform.State, c int) (*terraform.RemoteState, *httptest.Server) { 566 t.Helper() 567 568 var b64md5 string 569 buf := bytes.NewBuffer(nil) 570 571 cb := func(resp http.ResponseWriter, req *http.Request) { 572 if req.Method == "PUT" { 573 resp.WriteHeader(c) 574 return 575 } 576 if s == nil { 577 resp.WriteHeader(404) 578 return 579 } 580 581 resp.Header().Set("Content-MD5", b64md5) 582 resp.Write(buf.Bytes()) 583 } 584 585 srv := httptest.NewServer(http.HandlerFunc(cb)) 586 remote := &terraform.RemoteState{ 587 Type: "http", 588 Config: map[string]string{"address": srv.URL}, 589 } 590 591 if s != nil { 592 // Set the remote data 593 s.Remote = remote 594 595 enc := json.NewEncoder(buf) 596 if err := enc.Encode(s); err != nil { 597 t.Fatalf("err: %v", err) 598 } 599 md5 := md5.Sum(buf.Bytes()) 600 b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) 601 } 602 603 return remote, srv 604 } 605 606 // testlockState calls a separate process to the lock the state file at path. 607 // deferFunc should be called in the caller to properly unlock the file. 608 // Since many tests change the working durectory, the sourcedir argument must be 609 // supplied to locate the statelocker.go source. 610 func testLockState(sourceDir, path string) (func(), error) { 611 // build and run the binary ourselves so we can quickly terminate it for cleanup 612 buildDir, err := ioutil.TempDir(testingDir, "locker") 613 if err != nil { 614 return nil, err 615 } 616 cleanFunc := func() { 617 os.RemoveAll(buildDir) 618 } 619 620 source := filepath.Join(sourceDir, "statelocker.go") 621 lockBin := filepath.Join(buildDir, "statelocker") 622 623 out, err := exec.Command("go", "build", "-o", lockBin, source).CombinedOutput() 624 if err != nil { 625 cleanFunc() 626 return nil, fmt.Errorf("%s %s", err, out) 627 } 628 629 locker := exec.Command(lockBin, path) 630 pr, pw, err := os.Pipe() 631 if err != nil { 632 cleanFunc() 633 return nil, err 634 } 635 defer pr.Close() 636 defer pw.Close() 637 locker.Stderr = pw 638 locker.Stdout = pw 639 640 if err := locker.Start(); err != nil { 641 return nil, err 642 } 643 deferFunc := func() { 644 cleanFunc() 645 locker.Process.Signal(syscall.SIGTERM) 646 locker.Wait() 647 } 648 649 // wait for the process to lock 650 buf := make([]byte, 1024) 651 n, err := pr.Read(buf) 652 if err != nil { 653 return deferFunc, fmt.Errorf("read from statelocker returned: %s", err) 654 } 655 656 output := string(buf[:n]) 657 if !strings.HasPrefix(output, "LOCKID") { 658 return deferFunc, fmt.Errorf("statelocker wrote: %s", string(buf[:n])) 659 } 660 return deferFunc, nil 661 }