223 lines
5.9 KiB
Go
223 lines
5.9 KiB
Go
|
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)
|
||
|
}
|
||
|
|
||
|
}
|