github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/e2e/e2e.go (about)

     1  package e2e
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  
    12  	"github.com/hashicorp/terraform/internal/plans"
    13  	"github.com/hashicorp/terraform/internal/plans/planfile"
    14  	"github.com/hashicorp/terraform/internal/states"
    15  	"github.com/hashicorp/terraform/internal/states/statefile"
    16  )
    17  
    18  // Type binary represents the combination of a compiled binary
    19  // and a temporary working directory to run it in.
    20  type binary struct {
    21  	binPath string
    22  	workDir string
    23  	env     []string
    24  }
    25  
    26  // NewBinary prepares a temporary directory containing the files from the
    27  // given fixture and returns an instance of type binary that can run
    28  // the generated binary in that directory.
    29  //
    30  // If the temporary directory cannot be created, a fixture of the given name
    31  // cannot be found, or if an error occurs while _copying_ the fixture files,
    32  // this function will panic. Tests should be written to assume that this
    33  // function always succeeds.
    34  func NewBinary(binaryPath, workingDir string) *binary {
    35  	tmpDir, err := ioutil.TempDir("", "binary-e2etest")
    36  	if err != nil {
    37  		panic(err)
    38  	}
    39  
    40  	tmpDir, err = filepath.EvalSymlinks(tmpDir)
    41  	if err != nil {
    42  		panic(err)
    43  	}
    44  
    45  	// For our purposes here we do a very simplistic file copy that doesn't
    46  	// attempt to preserve file permissions, attributes, alternate data
    47  	// streams, etc. Since we only have to deal with our own fixtures in
    48  	// the testdata subdir, we know we don't need to deal with anything
    49  	// of this nature.
    50  	err = filepath.Walk(workingDir, func(path string, info os.FileInfo, err error) error {
    51  		if err != nil {
    52  			return err
    53  		}
    54  		if path == workingDir {
    55  			// nothing to do at the root
    56  			return nil
    57  		}
    58  
    59  		if filepath.Base(path) == ".exists" {
    60  			// We use this file just to let git know the "empty" fixture
    61  			// exists. It is not used by any test.
    62  			return nil
    63  		}
    64  
    65  		srcFn := path
    66  
    67  		path, err = filepath.Rel(workingDir, path)
    68  		if err != nil {
    69  			return err
    70  		}
    71  
    72  		dstFn := filepath.Join(tmpDir, path)
    73  
    74  		if info.IsDir() {
    75  			return os.Mkdir(dstFn, os.ModePerm)
    76  		}
    77  
    78  		src, err := os.Open(srcFn)
    79  		if err != nil {
    80  			return err
    81  		}
    82  		dst, err := os.OpenFile(dstFn, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.ModePerm)
    83  		if err != nil {
    84  			return err
    85  		}
    86  
    87  		_, err = io.Copy(dst, src)
    88  		if err != nil {
    89  			return err
    90  		}
    91  
    92  		if err := src.Close(); err != nil {
    93  			return err
    94  		}
    95  		if err := dst.Close(); err != nil {
    96  			return err
    97  		}
    98  
    99  		return nil
   100  	})
   101  	if err != nil {
   102  		panic(err)
   103  	}
   104  
   105  	return &binary{
   106  		binPath: binaryPath,
   107  		workDir: tmpDir,
   108  	}
   109  }
   110  
   111  // AddEnv appends an entry to the environment variable table passed to any
   112  // commands subsequently run.
   113  func (b *binary) AddEnv(entry string) {
   114  	b.env = append(b.env, entry)
   115  }
   116  
   117  // Cmd returns an exec.Cmd pre-configured to run the generated Terraform
   118  // binary with the given arguments in the temporary working directory.
   119  //
   120  // The returned object can be mutated by the caller to customize how the
   121  // process will be run, before calling Run.
   122  func (b *binary) Cmd(args ...string) *exec.Cmd {
   123  	cmd := exec.Command(b.binPath, args...)
   124  	cmd.Dir = b.workDir
   125  	cmd.Env = os.Environ()
   126  
   127  	// Disable checkpoint since we don't want to harass that service when
   128  	// our tests run. (This does, of course, mean we can't actually do
   129  	// end-to-end testing of our Checkpoint interactions.)
   130  	cmd.Env = append(cmd.Env, "CHECKPOINT_DISABLE=1")
   131  
   132  	cmd.Env = append(cmd.Env, b.env...)
   133  
   134  	return cmd
   135  }
   136  
   137  // Run executes the generated Terraform binary with the given arguments
   138  // and returns the bytes that it wrote to both stdout and stderr.
   139  //
   140  // This is a simple way to run Terraform for non-interactive commands
   141  // that don't need any special environment variables. For more complex
   142  // situations, use Cmd and customize the command before running it.
   143  func (b *binary) Run(args ...string) (stdout, stderr string, err error) {
   144  	cmd := b.Cmd(args...)
   145  	cmd.Stdin = nil
   146  	cmd.Stdout = &bytes.Buffer{}
   147  	cmd.Stderr = &bytes.Buffer{}
   148  	err = cmd.Run()
   149  	stdout = cmd.Stdout.(*bytes.Buffer).String()
   150  	stderr = cmd.Stderr.(*bytes.Buffer).String()
   151  	return
   152  }
   153  
   154  // Path returns a file path within the temporary working directory by
   155  // appending the given arguments as path segments.
   156  func (b *binary) Path(parts ...string) string {
   157  	args := make([]string, len(parts)+1)
   158  	args[0] = b.workDir
   159  	args = append(args, parts...)
   160  	return filepath.Join(args...)
   161  }
   162  
   163  // OpenFile is a helper for easily opening a file from the working directory
   164  // for reading.
   165  func (b *binary) OpenFile(path ...string) (*os.File, error) {
   166  	flatPath := b.Path(path...)
   167  	return os.Open(flatPath)
   168  }
   169  
   170  // ReadFile is a helper for easily reading a whole file from the working
   171  // directory.
   172  func (b *binary) ReadFile(path ...string) ([]byte, error) {
   173  	flatPath := b.Path(path...)
   174  	return ioutil.ReadFile(flatPath)
   175  }
   176  
   177  // FileExists is a helper for easily testing whether a particular file
   178  // exists in the working directory.
   179  func (b *binary) FileExists(path ...string) bool {
   180  	flatPath := b.Path(path...)
   181  	_, err := os.Stat(flatPath)
   182  	return !os.IsNotExist(err)
   183  }
   184  
   185  // LocalState is a helper for easily reading the local backend's state file
   186  // terraform.tfstate from the working directory.
   187  func (b *binary) LocalState() (*states.State, error) {
   188  	return b.StateFromFile("terraform.tfstate")
   189  }
   190  
   191  // StateFromFile is a helper for easily reading a state snapshot from a file
   192  // on disk relative to the working directory.
   193  func (b *binary) StateFromFile(filename string) (*states.State, error) {
   194  	f, err := b.OpenFile(filename)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	defer f.Close()
   199  
   200  	stateFile, err := statefile.Read(f)
   201  	if err != nil {
   202  		return nil, fmt.Errorf("Error reading statefile: %s", err)
   203  	}
   204  	return stateFile.State, nil
   205  }
   206  
   207  // Plan is a helper for easily reading a plan file from the working directory.
   208  func (b *binary) Plan(path string) (*plans.Plan, error) {
   209  	path = b.Path(path)
   210  	pr, err := planfile.Open(path)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	plan, err := pr.ReadPlan()
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	return plan, nil
   219  }
   220  
   221  // SetLocalState is a helper for easily writing to the file the local backend
   222  // uses for state in the working directory. This does not go through the
   223  // actual local backend code, so processing such as management of serials
   224  // does not apply and the given state will simply be written verbatim.
   225  func (b *binary) SetLocalState(state *states.State) error {
   226  	path := b.Path("terraform.tfstate")
   227  	f, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
   228  	if err != nil {
   229  		return fmt.Errorf("failed to create temporary state file %s: %s", path, err)
   230  	}
   231  	defer f.Close()
   232  
   233  	sf := &statefile.File{
   234  		Serial:  0,
   235  		Lineage: "fake-for-testing",
   236  		State:   state,
   237  	}
   238  	return statefile.Write(sf, f)
   239  }
   240  
   241  // Close cleans up the temporary resources associated with the object,
   242  // including its working directory. It is not valid to call Cmd or Run
   243  // after Close returns.
   244  //
   245  // This method does _not_ stop any running child processes. It's the
   246  // caller's responsibility to also terminate those _before_ closing the
   247  // underlying binary object.
   248  //
   249  // This function is designed to run under "defer", so it doesn't actually
   250  // do any error handling and will leave dangling temporary files on disk
   251  // if any errors occur while cleaning up.
   252  func (b *binary) Close() {
   253  	os.RemoveAll(b.workDir)
   254  }
   255  
   256  func GoBuild(pkgPath, tmpPrefix string) string {
   257  	dir, prefix := filepath.Split(tmpPrefix)
   258  	tmpFile, err := ioutil.TempFile(dir, prefix)
   259  	if err != nil {
   260  		panic(err)
   261  	}
   262  	tmpFilename := tmpFile.Name()
   263  	if err = tmpFile.Close(); err != nil {
   264  		panic(err)
   265  	}
   266  
   267  	cmd := exec.Command(
   268  		"go", "build",
   269  		"-o", tmpFilename,
   270  		pkgPath,
   271  	)
   272  	cmd.Stderr = os.Stderr
   273  	cmd.Stdout = os.Stdout
   274  
   275  	err = cmd.Run()
   276  	if err != nil {
   277  		// The go compiler will have already produced some error messages
   278  		// on stderr by the time we get here.
   279  		panic(fmt.Sprintf("failed to build executable: %s", err))
   280  	}
   281  
   282  	return tmpFilename
   283  }
   284  
   285  // WorkDir() returns the binary workdir
   286  func (b *binary) WorkDir() string {
   287  	return b.workDir
   288  }