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  }