github.com/bhameyie/otto@v0.2.1-0.20160406174117-16052efa52ec/helper/hashitools/project.go (about)

     1  package hashitools
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/go-checkpoint"
    14  	"github.com/hashicorp/go-version"
    15  )
    16  
    17  //go:generate go-bindata -pkg=hashitools -nomemcopy -nometadata ./data/...
    18  
    19  var (
    20  	versionRe = regexp.MustCompile(`v?(\d+\.\d+\.[^\s]+)`)
    21  )
    22  
    23  // Project represents a HashiCorp Go project and provides various operations
    24  // around that.
    25  type Project struct {
    26  	// Name is the name of the project, all lowercase
    27  	Name string
    28  
    29  	// Installer is the installer for this project
    30  	Installer Installer
    31  
    32  	// MinVersion is the minimum version of this project that Otto
    33  	// can use to function. This will be used with `InstallIfNeeded`
    34  	// to prompt the user to install.
    35  	MinVersion *version.Version
    36  }
    37  
    38  // InstallIfNeeded will check if installation of this project is required
    39  // and will invoke the installer if needed.
    40  func (p *Project) InstallIfNeeded() error {
    41  	log.Printf("[DEBUG] installIfNeeded: %s", p.Name)
    42  
    43  	// Start grabbing the latest version as early as possible since
    44  	// this requires a network call. We might as well do it while we're
    45  	// doing a subprocess.
    46  	latestCh := make(chan *version.Version, 1)
    47  	errCh := make(chan error, 1)
    48  	go func() {
    49  		latest, err := p.LatestVersion()
    50  		if err != nil {
    51  			errCh <- err
    52  		}
    53  		latestCh <- latest
    54  	}()
    55  
    56  	// Grab the version we have installed
    57  	installed, err := p.Version()
    58  	if err != nil {
    59  		return err
    60  	}
    61  	if installed == nil {
    62  		log.Printf("[DEBUG] installIfNeeded: %s not installed", p.Name)
    63  	} else {
    64  		log.Printf("[DEBUG] installIfNeeded: %s installed: %s", p.Name, installed)
    65  	}
    66  
    67  	// Wait for the latest
    68  	var latest *version.Version
    69  	select {
    70  	case latest = <-latestCh:
    71  	case err := <-errCh:
    72  		log.Printf("[ERROR] error checking latest version: %s", err)
    73  	}
    74  	log.Printf("[DEBUG] installIfNeeded: %s latest: %s", p.Name, latest)
    75  	log.Printf("[DEBUG] installIfNeeded: %s min: %s", p.Name, p.MinVersion)
    76  
    77  	// Determine if we require an install
    78  	installRequired := installed == nil
    79  	if installed != nil {
    80  		if installed.LessThan(p.MinVersion) {
    81  			installRequired = true
    82  		}
    83  
    84  		// TODO: updates
    85  	}
    86  
    87  	// No install required? Exit out.
    88  	if !installRequired {
    89  		log.Printf("[DEBUG] installIfNeeded: %s no installation needed", p.Name)
    90  		return nil
    91  	}
    92  
    93  	// We need to install! Ask the installer to verify for us
    94  	ok, err := p.Installer.InstallAsk(installed, p.MinVersion, latest)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	if !ok {
    99  		return fmt.Errorf("Installation cancelled")
   100  	}
   101  
   102  	// Install
   103  	return p.Installer.Install(latest)
   104  }
   105  
   106  // Latest version returns the latest version of this project.
   107  func (p *Project) LatestVersion() (*version.Version, error) {
   108  	check, err := checkpoint.Check(&checkpoint.CheckParams{
   109  		Product: p.Name,
   110  		Force:   true,
   111  	})
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	return version.NewVersion(check.CurrentVersion)
   117  }
   118  
   119  // Version reads the version of this project.
   120  func (p *Project) Version() (*version.Version, error) {
   121  	path := p.Path()
   122  	if !filepath.IsAbs(path) {
   123  		// Look for it on the path first if we don't have a full path to it
   124  		_, err := exec.LookPath(path)
   125  		if err != nil {
   126  			if execErr, ok := err.(*exec.Error); ok && execErr.Err == exec.ErrNotFound {
   127  				return nil, nil
   128  			}
   129  
   130  			return nil, err
   131  		}
   132  	}
   133  
   134  	// Grab the version
   135  	var stdout, buf bytes.Buffer
   136  	cmd := exec.Command(path, "--version")
   137  	cmd.Stdout = io.MultiWriter(&stdout, &buf)
   138  	cmd.Stderr = &buf
   139  	runErr := cmd.Run()
   140  
   141  	// Match the version out before we check for a run error, since some `project
   142  	// --version` commands can return a non-zero exit code.
   143  	matches := versionRe.FindStringSubmatch(stdout.String())
   144  	if len(matches) == 0 {
   145  		if runErr != nil {
   146  			return nil, fmt.Errorf(
   147  				"Error checking %s version: %s\n\n%s",
   148  				p.Name, runErr, buf.String())
   149  		}
   150  		return nil, fmt.Errorf(
   151  			"unable to find %s version in output: %q", p.Name, buf.String())
   152  	}
   153  
   154  	// This is a weird quirk: our version lib follows semver strictly
   155  	// but Vagrant in particular uses ".dev" instead "-dev" to end the
   156  	// version for Rubygems. Fix that up with duct tape.
   157  	matches[1] = strings.Replace(matches[1], ".dev", "-dev", 1)
   158  
   159  	return version.NewVersion(matches[1])
   160  }
   161  
   162  // Path returns the path to this project. This will check if the project
   163  // binary is pre-installed in our installation directory and use that path.
   164  // Otherwise, it will return the raw project name.
   165  func (p *Project) Path() string {
   166  	if p := p.Installer.Path(); p != "" {
   167  		return p
   168  	}
   169  
   170  	return p.Name
   171  }