github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/environs/tools/build.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package tools
     5  
     6  import (
     7  	"archive/tar"
     8  	"bytes"
     9  	"compress/gzip"
    10  	"crypto/sha256"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"github.com/juju/errors"
    20  	"github.com/juju/os/series"
    21  	"github.com/juju/utils/arch"
    22  	"github.com/juju/version"
    23  
    24  	"github.com/juju/juju/juju/names"
    25  	jujuversion "github.com/juju/juju/version"
    26  )
    27  
    28  // Archive writes the executable files found in the given directory in
    29  // gzipped tar format to w.
    30  func Archive(w io.Writer, dir string) error {
    31  	entries, err := ioutil.ReadDir(dir)
    32  	if err != nil {
    33  		return err
    34  	}
    35  
    36  	gzw := gzip.NewWriter(w)
    37  	defer closeErrorCheck(&err, gzw)
    38  
    39  	tarw := tar.NewWriter(gzw)
    40  	defer closeErrorCheck(&err, tarw)
    41  
    42  	for _, ent := range entries {
    43  		h := tarHeader(ent)
    44  		logger.Debugf("adding entry: %#v", h)
    45  		// ignore local umask
    46  		if isExecutable(ent) {
    47  			h.Mode = 0755
    48  		} else {
    49  			h.Mode = 0644
    50  		}
    51  		err := tarw.WriteHeader(h)
    52  		if err != nil {
    53  			return err
    54  		}
    55  		fileName := filepath.Join(dir, ent.Name())
    56  		if err := copyFile(tarw, fileName); err != nil {
    57  			return err
    58  		}
    59  	}
    60  	return nil
    61  }
    62  
    63  // archiveAndSHA256 calls Archive with the provided arguments,
    64  // and returns a hex-encoded SHA256 hash of the resulting
    65  // archive.
    66  func archiveAndSHA256(w io.Writer, dir string) (sha256hash string, err error) {
    67  	h := sha256.New()
    68  	if err := Archive(io.MultiWriter(h, w), dir); err != nil {
    69  		return "", err
    70  	}
    71  	return fmt.Sprintf("%x", h.Sum(nil)), err
    72  }
    73  
    74  // copyFile writes the contents of the given file to w.
    75  func copyFile(w io.Writer, file string) error {
    76  	f, err := os.Open(file)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	defer f.Close()
    81  	_, err = io.Copy(w, f)
    82  	return err
    83  }
    84  
    85  // tarHeader returns a tar file header given the file's stat
    86  // information.
    87  func tarHeader(i os.FileInfo) *tar.Header {
    88  	return &tar.Header{
    89  		Typeflag:   tar.TypeReg,
    90  		Name:       i.Name(),
    91  		Size:       i.Size(),
    92  		Mode:       int64(i.Mode() & 0777),
    93  		ModTime:    i.ModTime(),
    94  		AccessTime: i.ModTime(),
    95  		ChangeTime: i.ModTime(),
    96  		Uname:      "ubuntu",
    97  		Gname:      "ubuntu",
    98  	}
    99  }
   100  
   101  // isExecutable returns whether the given info
   102  // represents a regular file executable by (at least) the user.
   103  func isExecutable(i os.FileInfo) bool {
   104  	return i.Mode()&(0100|os.ModeType) == 0100
   105  }
   106  
   107  // closeErrorCheck means that we can ensure that
   108  // Close errors do not get lost even when we defer them,
   109  func closeErrorCheck(errp *error, c io.Closer) {
   110  	err := c.Close()
   111  	if *errp == nil {
   112  		*errp = err
   113  	}
   114  }
   115  
   116  func findExecutable(execFile string) (string, error) {
   117  	logger.Debugf("looking for: %s", execFile)
   118  	if filepath.IsAbs(execFile) {
   119  		return execFile, nil
   120  	}
   121  
   122  	dir, file := filepath.Split(execFile)
   123  
   124  	// Now we have two possibilities:
   125  	//   file == path indicating that the PATH was searched
   126  	//   dir != "" indicating that it is a relative path
   127  
   128  	if dir == "" {
   129  		path := os.Getenv("PATH")
   130  		for _, name := range filepath.SplitList(path) {
   131  			result := filepath.Join(name, file)
   132  			// Use exec.LookPath() to check if the file exists and is executable`
   133  			f, err := exec.LookPath(result)
   134  			if err == nil {
   135  				return f, nil
   136  			}
   137  		}
   138  
   139  		return "", fmt.Errorf("could not find %q in the path", file)
   140  	}
   141  	cwd, err := os.Getwd()
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	return filepath.Clean(filepath.Join(cwd, execFile)), nil
   146  }
   147  
   148  func copyFileWithMode(from, to string, mode os.FileMode) error {
   149  	source, err := os.Open(from)
   150  	if err != nil {
   151  		logger.Infof("open source failed: %v", err)
   152  		return err
   153  	}
   154  	defer source.Close()
   155  	destination, err := os.OpenFile(to, os.O_RDWR|os.O_TRUNC|os.O_CREATE, mode)
   156  	if err != nil {
   157  		logger.Infof("open destination failed: %v", err)
   158  		return err
   159  	}
   160  	defer destination.Close()
   161  	_, err = io.Copy(destination, source)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	return nil
   166  }
   167  
   168  // ExistingJujudLocation returns the directory to
   169  // a jujud executable in the path.
   170  func ExistingJujudLocation() (string, error) {
   171  	jujuLocation, err := findExecutable(os.Args[0])
   172  	if err != nil {
   173  		logger.Infof("%v", err)
   174  		return "", err
   175  	}
   176  	jujuDir := filepath.Dir(jujuLocation)
   177  	return jujuDir, nil
   178  }
   179  
   180  // VersionFileFallbackDir is the other location we'll check for a
   181  // juju-versions file if it's not alongside the binary (for example if
   182  // Juju was installed from a .deb). (Exposed so we can override it in
   183  // tests.)
   184  var VersionFileFallbackDir = "/usr/lib/juju"
   185  
   186  func copyExistingJujud(dir string) error {
   187  	// Assume that the user is running juju.
   188  	jujuDir, err := ExistingJujudLocation()
   189  	if err != nil {
   190  		logger.Infof("couldn't find existing jujud: %v", err)
   191  		return errors.Trace(err)
   192  	}
   193  	jujudLocation := filepath.Join(jujuDir, names.Jujud)
   194  	logger.Debugf("checking: %s", jujudLocation)
   195  	info, err := os.Stat(jujudLocation)
   196  	if err != nil {
   197  		logger.Infof("couldn't find existing jujud: %v", err)
   198  		return errors.Trace(err)
   199  	}
   200  	logger.Infof("Found agent binary to upload (%s)", jujudLocation)
   201  	target := filepath.Join(dir, names.Jujud)
   202  	logger.Infof("target: %v", target)
   203  	err = copyFileWithMode(jujudLocation, target, info.Mode())
   204  	if err != nil {
   205  		return errors.Trace(err)
   206  	}
   207  	// If there's a version file beside the jujud binary or in the
   208  	// fallback location, include that.
   209  	versionTarget := filepath.Join(dir, names.JujudVersions)
   210  
   211  	versionPaths := []string{
   212  		filepath.Join(jujuDir, names.JujudVersions),
   213  		filepath.Join(VersionFileFallbackDir, names.JujudVersions),
   214  	}
   215  	for _, versionPath := range versionPaths {
   216  		info, err = os.Stat(versionPath)
   217  		if os.IsNotExist(err) {
   218  			continue
   219  		} else if err != nil {
   220  			return errors.Trace(err)
   221  		}
   222  		logger.Infof("including versions file %q", versionPath)
   223  		return errors.Trace(copyFileWithMode(versionPath, versionTarget, info.Mode()))
   224  	}
   225  	return nil
   226  }
   227  
   228  func buildJujud(dir string) error {
   229  	logger.Infof("building jujud")
   230  	cmds := [][]string{
   231  		{"go", "build", "-gccgoflags=-static-libgo", "-o", filepath.Join(dir, names.Jujud), "github.com/juju/juju/cmd/jujud"},
   232  	}
   233  	for _, args := range cmds {
   234  		cmd := exec.Command(args[0], args[1:]...)
   235  		out, err := cmd.CombinedOutput()
   236  		if err != nil {
   237  			return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out)
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  func packageLocalTools(toolsDir string, buildAgent bool) error {
   244  	if !buildAgent {
   245  		if err := copyExistingJujud(toolsDir); err != nil {
   246  			return errors.New("no prepackaged agent available and no jujud binary can be found")
   247  		}
   248  		return nil
   249  	}
   250  	logger.Infof("Building agent binary to upload (%s)", jujuversion.Current.String())
   251  	if err := buildJujud(toolsDir); err != nil {
   252  		return errors.Annotate(err, "cannot build jujud agent binary from source")
   253  	}
   254  	return nil
   255  }
   256  
   257  // BundleToolsFunc is a function which can bundle all the current juju tools
   258  // in gzipped tar format to the given writer.
   259  type BundleToolsFunc func(build bool, w io.Writer, forceVersion *version.Number) (version.Binary, bool, string, error)
   260  
   261  // Override for testing.
   262  var BundleTools BundleToolsFunc = bundleTools
   263  
   264  // bundleTools bundles all the current juju tools in gzipped tar
   265  // format to the given writer.  If forceVersion is not nil and the
   266  // file isn't an official build, a FORCE-VERSION file is included in
   267  // the tools bundle so it will lie about its current version number.
   268  func bundleTools(build bool, w io.Writer, forceVersion *version.Number) (_ version.Binary, official bool, sha256hash string, _ error) {
   269  	dir, err := ioutil.TempDir("", "juju-tools")
   270  	if err != nil {
   271  		return version.Binary{}, false, "", err
   272  	}
   273  	defer os.RemoveAll(dir)
   274  	if err := packageLocalTools(dir, build); err != nil {
   275  		return version.Binary{}, false, "", err
   276  	}
   277  
   278  	tvers, official, err := JujudVersion(dir)
   279  	if err != nil {
   280  		return version.Binary{}, false, "", errors.Trace(err)
   281  	}
   282  	if official {
   283  		logger.Debugf("using official version %s", tvers)
   284  	} else if forceVersion != nil {
   285  		logger.Debugf("forcing version to %s", forceVersion)
   286  		if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil {
   287  			return version.Binary{}, false, "", err
   288  		}
   289  	}
   290  
   291  	sha256hash, err = archiveAndSHA256(w, dir)
   292  	if err != nil {
   293  		return version.Binary{}, false, "", err
   294  	}
   295  	return tvers, official, sha256hash, err
   296  }
   297  
   298  var execCommand = exec.Command
   299  
   300  func getVersionFromJujud(dir string) (version.Binary, error) {
   301  	path := filepath.Join(dir, names.Jujud)
   302  	cmd := execCommand(path, "version")
   303  	var stdout, stderr bytes.Buffer
   304  	cmd.Stdout = &stdout
   305  	cmd.Stderr = &stderr
   306  
   307  	if err := cmd.Run(); err != nil {
   308  		return version.Binary{}, errors.Errorf("cannot get version from %q: %v; %s", path, err, stderr.String()+stdout.String())
   309  	}
   310  	tvs := strings.TrimSpace(stdout.String())
   311  	tvers, err := version.ParseBinary(tvs)
   312  	if err != nil {
   313  		return version.Binary{}, errors.Errorf("invalid version %q printed by jujud", tvs)
   314  	}
   315  	return tvers, nil
   316  }
   317  
   318  // JujudVersion returns the Jujud version at the specified location,
   319  // and whether it is an official binary.
   320  func JujudVersion(dir string) (version.Binary, bool, error) {
   321  	tvers, err := getVersionFromFile(dir)
   322  	official := err == nil
   323  	if err != nil && !errors.IsNotFound(err) && !isNoMatchingToolsChecksum(err) {
   324  		return version.Binary{}, false, errors.Trace(err)
   325  	}
   326  	if errors.IsNotFound(err) || isNoMatchingToolsChecksum(err) {
   327  		// No signature file found.
   328  		// Extract the version number that the jujud binary was built with.
   329  		// This is used to check compatibility with the version of the client
   330  		// being used to bootstrap.
   331  		tvers, err = getVersionFromJujud(dir)
   332  		if err != nil {
   333  			return version.Binary{}, false, errors.Trace(err)
   334  		}
   335  	}
   336  	return tvers, official, nil
   337  }
   338  
   339  type noMatchingToolsChecksum struct {
   340  	versionPath string
   341  	jujudPath   string
   342  }
   343  
   344  func (e *noMatchingToolsChecksum) Error() string {
   345  	return fmt.Sprintf("no SHA256 in version file %q matches binary %q", e.versionPath, e.jujudPath)
   346  }
   347  
   348  func isNoMatchingToolsChecksum(err error) bool {
   349  	_, ok := err.(*noMatchingToolsChecksum)
   350  	return ok
   351  }
   352  
   353  func getVersionFromFile(dir string) (version.Binary, error) {
   354  	versionPath := filepath.Join(dir, names.JujudVersions)
   355  	sigFile, err := os.Open(versionPath)
   356  	if os.IsNotExist(err) {
   357  		return version.Binary{}, errors.NotFoundf("version file %q", versionPath)
   358  	} else if err != nil {
   359  		return version.Binary{}, errors.Trace(err)
   360  	}
   361  	defer sigFile.Close()
   362  
   363  	versions, err := ParseVersions(sigFile)
   364  	if err != nil {
   365  		return version.Binary{}, errors.Trace(err)
   366  	}
   367  
   368  	// Find the binary by hash.
   369  	jujudPath := filepath.Join(dir, names.Jujud)
   370  	jujudFile, err := os.Open(jujudPath)
   371  	if err != nil {
   372  		return version.Binary{}, errors.Trace(err)
   373  	}
   374  	defer jujudFile.Close()
   375  	matching, err := versions.VersionsMatching(jujudFile)
   376  	if err != nil {
   377  		return version.Binary{}, errors.Trace(err)
   378  	}
   379  	if len(matching) == 0 {
   380  		return version.Binary{}, &noMatchingToolsChecksum{versionPath, jujudPath}
   381  	}
   382  	return selectBinary(matching)
   383  }
   384  
   385  func selectBinary(versions []string) (version.Binary, error) {
   386  	thisArch := arch.HostArch()
   387  	thisSeries, err := series.HostSeries()
   388  	if err != nil {
   389  		return version.Binary{}, errors.Trace(err)
   390  	}
   391  	var current version.Binary
   392  	for _, ver := range versions {
   393  		current, err = version.ParseBinary(ver)
   394  		if err != nil {
   395  			return version.Binary{}, errors.Trace(err)
   396  		}
   397  		if current.Series == thisSeries && current.Arch == thisArch {
   398  			return current, nil
   399  		}
   400  	}
   401  	// There's no version matching our series/arch, but the signature
   402  	// still matches the binary for all versions passed in, so just
   403  	// punt.
   404  	return current, nil
   405  }