github.com/smithx10/nomad@v0.9.1-rc1/e2e/cli/command/environment.go (about) 1 package command 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "os" 10 "os/exec" 11 "os/signal" 12 "path" 13 "path/filepath" 14 "strings" 15 "syscall" 16 "time" 17 18 hclog "github.com/hashicorp/go-hclog" 19 ) 20 21 // environment captures all the information needed to execute terraform 22 // in order to setup a test environment 23 type environment struct { 24 provider string // provider ex. aws 25 name string // environment name ex. generic 26 27 tf string // location of terraform binary 28 tfPath string // path to terraform configuration 29 tfState string // path to terraform state file 30 logger hclog.Logger 31 } 32 33 func (env *environment) canonicalName() string { 34 return fmt.Sprintf("%s/%s", env.provider, env.name) 35 } 36 37 // envResults are the fields returned after provisioning a test environment 38 type envResults struct { 39 nomadAddr string 40 consulAddr string 41 vaultAddr string 42 } 43 44 // newEnv takes a path to the environments directory, environment name and provider, 45 // path to terraform state file and a logger and builds the environment stuct used 46 // to initial terraform calls 47 func newEnv(envPath, provider, name, tfStatePath string, logger hclog.Logger) (*environment, error) { 48 // Make sure terraform is on the PATH 49 tf, err := exec.LookPath("terraform") 50 if err != nil { 51 return nil, fmt.Errorf("failed to lookup terraform binary: %v", err) 52 } 53 54 logger = logger.Named("provision").With("provider", provider, "name", name) 55 56 // set the path to the terraform module 57 tfPath := path.Join(envPath, provider, name) 58 logger.Debug("using tf path", "path", tfPath) 59 if _, err := os.Stat(tfPath); os.IsNotExist(err) { 60 return nil, fmt.Errorf("failed to lookup terraform configuration dir %s: %v", tfPath, err) 61 } 62 63 // set the path to state file 64 tfState := path.Join(tfStatePath, fmt.Sprintf("e2e.%s.%s.tfstate", provider, name)) 65 66 env := &environment{ 67 provider: provider, 68 name: name, 69 tf: tf, 70 tfPath: tfPath, 71 tfState: tfState, 72 logger: logger, 73 } 74 return env, nil 75 } 76 77 // envsFromGlob allows for the discovery of multiple environments using globs (*). 78 // ex. aws/* for all environments in aws. 79 func envsFromGlob(envPath, glob, tfStatePath string, logger hclog.Logger) ([]*environment, error) { 80 results, err := filepath.Glob(filepath.Join(envPath, glob)) 81 if err != nil { 82 return nil, err 83 } 84 85 envs := []*environment{} 86 87 for _, p := range results { 88 elems := strings.Split(p, "/") 89 name := elems[len(elems)-1] 90 provider := elems[len(elems)-2] 91 env, err := newEnv(envPath, provider, name, tfStatePath, logger) 92 if err != nil { 93 return nil, err 94 } 95 96 envs = append(envs, env) 97 } 98 99 return envs, nil 100 } 101 102 // provision calls terraform to setup the environment with the given nomad binary 103 func (env *environment) provision(nomadPath string) (*envResults, error) { 104 tfArgs := []string{"apply", "-auto-approve", "-input=false", "-no-color", 105 "-state", env.tfState, 106 "-var", fmt.Sprintf("nomad_binary=%s", path.Join(nomadPath, "nomad")), 107 env.tfPath, 108 } 109 110 // Setup the 'terraform apply' command 111 ctx := context.Background() 112 cmd := exec.CommandContext(ctx, env.tf, tfArgs...) 113 114 // Funnel the stdout/stderr to logging 115 stderr, err := cmd.StderrPipe() 116 if err != nil { 117 return nil, fmt.Errorf("failed to get stderr pipe: %v", err) 118 } 119 stdout, err := cmd.StdoutPipe() 120 if err != nil { 121 return nil, fmt.Errorf("failed to get stdout pipe: %v", err) 122 } 123 124 // Run 'terraform apply' 125 cmd.Start() 126 go tfLog(env.logger.Named("tf.stderr"), stderr) 127 go tfLog(env.logger.Named("tf.stdout"), stdout) 128 129 sigChan := make(chan os.Signal) 130 signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) 131 132 cmdChan := make(chan error) 133 go func() { 134 cmdChan <- cmd.Wait() 135 }() 136 137 // if an interrupt is received before terraform finished, forward signal to 138 // child pid 139 select { 140 case sig := <-sigChan: 141 env.logger.Error("interrupt received, forwarding signal to child process", 142 "pid", cmd.Process.Pid) 143 cmd.Process.Signal(sig) 144 if err := procWaitTimeout(cmd.Process, 5*time.Second); err != nil { 145 env.logger.Error("child process did not exit in time, killing forcefully", 146 "pid", cmd.Process.Pid) 147 cmd.Process.Kill() 148 } 149 return nil, fmt.Errorf("interrupt received") 150 case err := <-cmdChan: 151 if err != nil { 152 return nil, fmt.Errorf("terraform exited with a non-zero status: %v", err) 153 } 154 } 155 156 // Setup and run 'terraform output' to get the module output 157 cmd = exec.CommandContext(ctx, env.tf, "output", "-json", "-state", env.tfState) 158 out, err := cmd.Output() 159 if err != nil { 160 return nil, fmt.Errorf("terraform exited with a non-zero status: %v", err) 161 } 162 163 // Parse the json and pull out results 164 tfOutput := make(map[string]map[string]interface{}) 165 err = json.Unmarshal(out, &tfOutput) 166 if err != nil { 167 return nil, fmt.Errorf("failed to parse terraform output: %v", err) 168 } 169 170 results := &envResults{} 171 if nomadAddr, ok := tfOutput["nomad_addr"]; ok { 172 results.nomadAddr = nomadAddr["value"].(string) 173 } else { 174 return nil, fmt.Errorf("'nomad_addr' field expected in terraform output, but was missing") 175 } 176 177 return results, nil 178 } 179 180 // destroy calls terraform to destroy the environment 181 func (env *environment) destroy() error { 182 tfArgs := []string{"destroy", "-auto-approve", "-no-color", 183 "-state", env.tfState, 184 "-var", "nomad_binary=", 185 env.tfPath, 186 } 187 cmd := exec.Command(env.tf, tfArgs...) 188 189 // Funnel the stdout/stderr to logging 190 stderr, err := cmd.StderrPipe() 191 if err != nil { 192 return fmt.Errorf("failed to get stderr pipe: %v", err) 193 } 194 stdout, err := cmd.StdoutPipe() 195 if err != nil { 196 return fmt.Errorf("failed to get stdout pipe: %v", err) 197 } 198 199 // Run 'terraform destroy' 200 cmd.Start() 201 go tfLog(env.logger.Named("tf.stderr"), stderr) 202 go tfLog(env.logger.Named("tf.stdout"), stdout) 203 204 err = cmd.Wait() 205 if err != nil { 206 return fmt.Errorf("terraform exited with a non-zero status: %v", err) 207 } 208 209 return nil 210 } 211 212 func tfLog(logger hclog.Logger, r io.ReadCloser) { 213 defer r.Close() 214 scanner := bufio.NewScanner(r) 215 for scanner.Scan() { 216 logger.Debug(scanner.Text()) 217 } 218 if err := scanner.Err(); err != nil { 219 logger.Error("scan error", "error", err) 220 } 221 222 }