diff --git a/command/e2etest/init_test.go b/command/e2etest/init_test.go index 7a5ec2ab2..bbfc3a6ae 100644 --- a/command/e2etest/init_test.go +++ b/command/e2etest/init_test.go @@ -1,8 +1,11 @@ package e2etest import ( + "path/filepath" "strings" "testing" + + "github.com/hashicorp/terraform/e2e" ) func TestInitProviders(t *testing.T) { @@ -15,7 +18,8 @@ func TestInitProviders(t *testing.T) { // is to test the interaction with the real repository. skipIfCannotAccessNetwork(t) - tf := newTerraform("template-provider") + fixturePath := filepath.Join("test-fixtures", "template-provider") + tf := e2e.NewBinary(terraformBin, fixturePath) defer tf.Close() stdout, stderr, err := tf.Run("init") diff --git a/command/e2etest/main_test.go b/command/e2etest/main_test.go index 33a327976..61c64a30b 100644 --- a/command/e2etest/main_test.go +++ b/command/e2etest/main_test.go @@ -1,16 +1,12 @@ package e2etest import ( - "bytes" "fmt" - "io" - "io/ioutil" "os" - "os/exec" "path/filepath" "testing" - tfcore "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/e2e" ) var terraformBin string @@ -37,29 +33,7 @@ func setup() func() { return func() {} } - tmpFile, err := ioutil.TempFile("", "terraform") - if err != nil { - panic(err) - } - tmpFilename := tmpFile.Name() - if err = tmpFile.Close(); err != nil { - panic(err) - } - - cmd := exec.Command( - "go", "build", - "-o", tmpFilename, - "github.com/hashicorp/terraform", - ) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - - err = cmd.Run() - if err != nil { - // The go compiler will have already produced some error messages - // on stderr by the time we get here. - panic(fmt.Sprintf("failed to build terraform executable: %s", err)) - } + tmpFilename := e2e.GoBuild("github.com/hashicorp/terraform", "terraform") // Make the executable available for use in tests terraformBin = tmpFilename @@ -81,209 +55,3 @@ func skipIfCannotAccessNetwork(t *testing.T) { t.Skip("network access not allowed; use TF_ACC=1 to enable") } } - -// Type terraform represents the combination of a compiled Terraform binary -// and a temporary working directory to run it in. -// -// This is the main harness for tests in this package. -type terraform struct { - bin string - dir string -} - -// newTerraform prepares a temporary directory containing the files from the -// given fixture and returns an instance of type terraform that can run -// the generated Terraform binary in that directory. -// -// If the temporary directory cannot be created, a fixture of the given name -// cannot be found, or if an error occurs while _copying_ the fixture files, -// this function will panic. Tests should be written to assume that this -// function always succeeds. -func newTerraform(fixtureName string) *terraform { - tmpDir, err := ioutil.TempDir("", "terraform-e2etest") - if err != nil { - panic(err) - } - - // For our purposes here we do a very simplistic file copy that doesn't - // attempt to preserve file permissions, attributes, alternate data - // streams, etc. Since we only have to deal with our own fixtures in - // the test-fixtures subdir, we know we don't need to deal with anything - // of this nature. - srcDir := filepath.Join("test-fixtures", fixtureName) - err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if path == srcDir { - // nothing to do at the root - return nil - } - - srcFn := path - - path, err = filepath.Rel(srcDir, path) - if err != nil { - return err - } - - dstFn := filepath.Join(tmpDir, path) - - if info.IsDir() { - return os.Mkdir(dstFn, os.ModePerm) - } - - src, err := os.Open(srcFn) - if err != nil { - return err - } - dst, err := os.OpenFile(dstFn, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) - if err != nil { - return err - } - - _, err = io.Copy(dst, src) - if err != nil { - return err - } - - if err := src.Close(); err != nil { - return err - } - if err := dst.Close(); err != nil { - return err - } - - return nil - }) - if err != nil { - panic(err) - } - - return &terraform{ - bin: terraformBin, - dir: tmpDir, - } -} - -// Cmd returns an exec.Cmd pre-configured to run the generated Terraform -// binary with the given arguments in the temporary working directory. -// -// The returned object can be mutated by the caller to customize how the -// process will be run, before calling Run. -func (t *terraform) Cmd(args ...string) *exec.Cmd { - cmd := exec.Command(t.bin, args...) - cmd.Dir = t.dir - cmd.Env = os.Environ() - - // Disable checkpoint since we don't want to harass that service when - // our tests run. (This does, of course, mean we can't actually do - // end-to-end testing of our Checkpoint interactions.) - cmd.Env = append(cmd.Env, "CHECKPOINT_DISABLE=1") - - return cmd -} - -// Run executes the generated Terraform binary with the given arguments -// and returns the bytes that it wrote to both stdout and stderr. -// -// This is a simple way to run Terraform for non-interactive commands -// that don't need any special environment variables. For more complex -// situations, use Cmd and customize the command before running it. -func (t *terraform) Run(args ...string) (stdout, stderr string, err error) { - cmd := t.Cmd(args...) - cmd.Stdin = nil - cmd.Stdout = &bytes.Buffer{} - cmd.Stderr = &bytes.Buffer{} - err = cmd.Run() - stdout = cmd.Stdout.(*bytes.Buffer).String() - stderr = cmd.Stderr.(*bytes.Buffer).String() - return -} - -// Path returns a file path within the temporary working directory by -// appending the given arguments as path segments. -func (t *terraform) Path(parts ...string) string { - args := make([]string, len(parts)+1) - args[0] = t.dir - args = append(args, parts...) - return filepath.Join(args...) -} - -// OpenFile is a helper for easily opening a file from the working directory -// for reading. -func (t *terraform) OpenFile(path ...string) (*os.File, error) { - flatPath := t.Path(path...) - return os.Open(flatPath) -} - -// ReadFile is a helper for easily reading a whole file from the working -// directory. -func (t *terraform) ReadFile(path ...string) ([]byte, error) { - flatPath := t.Path(path...) - return ioutil.ReadFile(flatPath) -} - -// FileExists is a helper for easily testing whether a particular file -// exists in the working directory. -func (t *terraform) FileExists(path ...string) bool { - flatPath := t.Path(path...) - _, err := os.Stat(flatPath) - return !os.IsNotExist(err) -} - -// LocalState is a helper for easily reading the local backend's state file -// terraform.tfstate from the working directory. -func (t *terraform) LocalState() (*tfcore.State, error) { - f, err := t.OpenFile("terraform.tfstate") - if err != nil { - return nil, err - } - defer f.Close() - return tfcore.ReadState(f) -} - -// Plan is a helper for easily reading a plan file from the working directory. -func (t *terraform) Plan(path ...string) (*tfcore.Plan, error) { - f, err := t.OpenFile(path...) - if err != nil { - return nil, err - } - defer f.Close() - return tfcore.ReadPlan(f) -} - -// SetLocalState is a helper for easily writing to the file the local backend -// uses for state in the working directory. This does not go through the -// actual local backend code, so processing such as management of serials -// does not apply and the given state will simply be written verbatim. -func (t *terraform) SetLocalState(state *tfcore.State) error { - path := t.Path("terraform.tfstate") - f, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm) - if err != nil { - return err - } - defer func() { - err := f.Close() - if err != nil { - panic(fmt.Sprintf("failed to close state file after writing: %s", err)) - } - }() - - return tfcore.WriteState(state, f) -} - -// Close cleans up the temporary resources associated with the object, -// including its working directory. It is not valid to call Cmd or Run -// after Close returns. -// -// This method does _not_ stop any running child processes. It's the -// caller's responsibility to also terminate those _before_ closing the -// underlying terraform object. -// -// This function is designed to run under "defer", so it doesn't actually -// do any error handling and will leave dangling temporary files on disk -// if any errors occur while cleaning up. -func (t *terraform) Close() { - os.RemoveAll(t.dir) -} diff --git a/command/e2etest/primary_test.go b/command/e2etest/primary_test.go index f491683c9..e0bccacc5 100644 --- a/command/e2etest/primary_test.go +++ b/command/e2etest/primary_test.go @@ -1,12 +1,14 @@ package e2etest import ( + "path/filepath" "reflect" "sort" "strings" "testing" "github.com/davecgh/go-spew/spew" + "github.com/hashicorp/terraform/e2e" ) // The tests in this file are for the "primary workflow", which includes @@ -24,7 +26,8 @@ func TestPrimarySeparatePlan(t *testing.T) { // allowed. skipIfCannotAccessNetwork(t) - tf := newTerraform("full-workflow-null") + fixturePath := filepath.Join("test-fixtures", "full-workflow-null") + tf := e2e.NewBinary(terraformBin, fixturePath) defer tf.Close() //// INIT diff --git a/command/e2etest/version_test.go b/command/e2etest/version_test.go index d8610e98b..57a882c6f 100644 --- a/command/e2etest/version_test.go +++ b/command/e2etest/version_test.go @@ -2,9 +2,11 @@ package e2etest import ( "fmt" + "path/filepath" "strings" "testing" + "github.com/hashicorp/terraform/e2e" tfcore "github.com/hashicorp/terraform/terraform" ) @@ -16,7 +18,8 @@ func TestVersion(t *testing.T) { t.Parallel() - tf := newTerraform("empty") + fixturePath := filepath.Join("test-fixtures", "empty") + tf := e2e.NewBinary(terraformBin, fixturePath) defer tf.Close() stdout, stderr, err := tf.Run("version") diff --git a/e2e/e2e.go b/e2e/e2e.go new file mode 100644 index 000000000..dbc457e1c --- /dev/null +++ b/e2e/e2e.go @@ -0,0 +1,244 @@ +package e2e + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + tfcore "github.com/hashicorp/terraform/terraform" +) + +// Type binary represents the combination of a compiled binary +// and a temporary working directory to run it in. +type binary struct { + binPath string + workDir string +} + +// NewBinary prepares a temporary directory containing the files from the +// given fixture and returns an instance of type binary that can run +// the generated binary in that directory. +// +// If the temporary directory cannot be created, a fixture of the given name +// cannot be found, or if an error occurs while _copying_ the fixture files, +// this function will panic. Tests should be written to assume that this +// function always succeeds. +func NewBinary(binaryPath, workingDir string) *binary { + tmpDir, err := ioutil.TempDir("", "binary-e2etest") + if err != nil { + panic(err) + } + + // For our purposes here we do a very simplistic file copy that doesn't + // attempt to preserve file permissions, attributes, alternate data + // streams, etc. Since we only have to deal with our own fixtures in + // the test-fixtures subdir, we know we don't need to deal with anything + // of this nature. + err = filepath.Walk(workingDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == workingDir { + // nothing to do at the root + return nil + } + + srcFn := path + + path, err = filepath.Rel(workingDir, path) + if err != nil { + return err + } + + dstFn := filepath.Join(tmpDir, path) + + if info.IsDir() { + return os.Mkdir(dstFn, os.ModePerm) + } + + src, err := os.Open(srcFn) + if err != nil { + return err + } + dst, err := os.OpenFile(dstFn, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + + _, err = io.Copy(dst, src) + if err != nil { + return err + } + + if err := src.Close(); err != nil { + return err + } + if err := dst.Close(); err != nil { + return err + } + + return nil + }) + if err != nil { + panic(err) + } + + return &binary{ + binPath: binaryPath, + workDir: tmpDir, + } +} + +// Cmd returns an exec.Cmd pre-configured to run the generated Terraform +// binary with the given arguments in the temporary working directory. +// +// The returned object can be mutated by the caller to customize how the +// process will be run, before calling Run. +func (b *binary) Cmd(args ...string) *exec.Cmd { + cmd := exec.Command(b.binPath, args...) + cmd.Dir = b.workDir + cmd.Env = os.Environ() + + // Disable checkpoint since we don't want to harass that service when + // our tests run. (This does, of course, mean we can't actually do + // end-to-end testing of our Checkpoint interactions.) + cmd.Env = append(cmd.Env, "CHECKPOINT_DISABLE=1") + + return cmd +} + +// Run executes the generated Terraform binary with the given arguments +// and returns the bytes that it wrote to both stdout and stderr. +// +// This is a simple way to run Terraform for non-interactive commands +// that don't need any special environment variables. For more complex +// situations, use Cmd and customize the command before running it. +func (b *binary) Run(args ...string) (stdout, stderr string, err error) { + cmd := b.Cmd(args...) + cmd.Stdin = nil + cmd.Stdout = &bytes.Buffer{} + cmd.Stderr = &bytes.Buffer{} + err = cmd.Run() + stdout = cmd.Stdout.(*bytes.Buffer).String() + stderr = cmd.Stderr.(*bytes.Buffer).String() + return +} + +// Path returns a file path within the temporary working directory by +// appending the given arguments as path segments. +func (b *binary) Path(parts ...string) string { + args := make([]string, len(parts)+1) + args[0] = b.workDir + args = append(args, parts...) + return filepath.Join(args...) +} + +// OpenFile is a helper for easily opening a file from the working directory +// for reading. +func (b *binary) OpenFile(path ...string) (*os.File, error) { + flatPath := b.Path(path...) + return os.Open(flatPath) +} + +// ReadFile is a helper for easily reading a whole file from the working +// directory. +func (b *binary) ReadFile(path ...string) ([]byte, error) { + flatPath := b.Path(path...) + return ioutil.ReadFile(flatPath) +} + +// FileExists is a helper for easily testing whether a particular file +// exists in the working directory. +func (b *binary) FileExists(path ...string) bool { + flatPath := b.Path(path...) + _, err := os.Stat(flatPath) + return !os.IsNotExist(err) +} + +// LocalState is a helper for easily reading the local backend's state file +// terraform.tfstate from the working directory. +func (b *binary) LocalState() (*tfcore.State, error) { + f, err := b.OpenFile("terraform.tfstate") + if err != nil { + return nil, err + } + defer f.Close() + return tfcore.ReadState(f) +} + +// Plan is a helper for easily reading a plan file from the working directory. +func (b *binary) Plan(path ...string) (*tfcore.Plan, error) { + f, err := b.OpenFile(path...) + if err != nil { + return nil, err + } + defer f.Close() + return tfcore.ReadPlan(f) +} + +// SetLocalState is a helper for easily writing to the file the local backend +// uses for state in the working directory. This does not go through the +// actual local backend code, so processing such as management of serials +// does not apply and the given state will simply be written verbatim. +func (b *binary) SetLocalState(state *tfcore.State) error { + path := b.Path("terraform.tfstate") + f, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm) + if err != nil { + return err + } + defer func() { + err := f.Close() + if err != nil { + panic(fmt.Sprintf("failed to close state file after writing: %s", err)) + } + }() + + return tfcore.WriteState(state, f) +} + +// Close cleans up the temporary resources associated with the object, +// including its working directory. It is not valid to call Cmd or Run +// after Close returns. +// +// This method does _not_ stop any running child processes. It's the +// caller's responsibility to also terminate those _before_ closing the +// underlying binary object. +// +// This function is designed to run under "defer", so it doesn't actually +// do any error handling and will leave dangling temporary files on disk +// if any errors occur while cleaning up. +func (b *binary) Close() { + os.RemoveAll(b.workDir) +} + +func GoBuild(pkgPath, tmpPrefix string) string { + tmpFile, err := ioutil.TempFile("", tmpPrefix) + if err != nil { + panic(err) + } + tmpFilename := tmpFile.Name() + if err = tmpFile.Close(); err != nil { + panic(err) + } + + cmd := exec.Command( + "go", "build", + "-o", tmpFilename, + pkgPath, + ) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + + err = cmd.Run() + if err != nil { + // The go compiler will have already produced some error messages + // on stderr by the time we get here. + panic(fmt.Sprintf("failed to build executable: %s", err)) + } + + return tmpFilename +}