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 }