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