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