github.com/ticketmaster/terraform@v0.10.0-beta2.0.20170711045249-a12daf5aba4f/tools/terraform-bundle/package.go (about)

     1  package main
     2  
     3  import (
     4  	"archive/zip"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"time"
    11  
    12  	"flag"
    13  
    14  	"io"
    15  
    16  	getter "github.com/hashicorp/go-getter"
    17  	"github.com/hashicorp/terraform/plugin"
    18  	"github.com/hashicorp/terraform/plugin/discovery"
    19  	"github.com/mitchellh/cli"
    20  )
    21  
    22  const releasesBaseURL = "https://releases.hashicorp.com"
    23  
    24  type PackageCommand struct {
    25  	ui cli.Ui
    26  }
    27  
    28  func (c *PackageCommand) Run(args []string) int {
    29  	flags := flag.NewFlagSet("package", flag.ExitOnError)
    30  	osPtr := flags.String("os", "", "Target operating system")
    31  	archPtr := flags.String("arch", "", "Target CPU architecture")
    32  	err := flags.Parse(args)
    33  	if err != nil {
    34  		c.ui.Error(err.Error())
    35  		return 1
    36  	}
    37  
    38  	osName := runtime.GOOS
    39  	archName := runtime.GOARCH
    40  	if *osPtr != "" {
    41  		osName = *osPtr
    42  	}
    43  	if *archPtr != "" {
    44  		archName = *archPtr
    45  	}
    46  
    47  	if flags.NArg() != 1 {
    48  		c.ui.Error("Configuration filename is required")
    49  		return 1
    50  	}
    51  	configFn := flags.Arg(0)
    52  
    53  	config, err := LoadConfigFile(configFn)
    54  	if err != nil {
    55  		c.ui.Error(fmt.Sprintf("Failed to read config: %s", err))
    56  		return 1
    57  	}
    58  
    59  	if discovery.ConstraintStr("< 0.10.0-beta1").MustParse().Allows(config.Terraform.Version.MustParse()) {
    60  		c.ui.Error("Bundles can be created only for Terraform 0.10 or newer")
    61  		return 1
    62  	}
    63  
    64  	workDir, err := ioutil.TempDir("", "terraform-bundle")
    65  	if err != nil {
    66  		c.ui.Error(fmt.Sprintf("Could not create temporary dir: %s", err))
    67  		return 1
    68  	}
    69  	defer os.RemoveAll(workDir)
    70  
    71  	c.ui.Info(fmt.Sprintf("Fetching Terraform %s core package...", config.Terraform.Version))
    72  
    73  	coreZipURL := c.coreURL(config.Terraform.Version, osName, archName)
    74  	err = getter.Get(workDir, coreZipURL)
    75  	if err != nil {
    76  		c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err))
    77  	}
    78  
    79  	installer := &discovery.ProviderInstaller{
    80  		Dir: workDir,
    81  
    82  		// FIXME: This is incorrect because it uses the protocol version of
    83  		// this tool, rather than of the Terraform binary we just downloaded.
    84  		// But we can't get this information from a Terraform binary, so
    85  		// we'll just ignore this for now as we only have one protocol version
    86  		// in play anyway. If a new protocol version shows up later we will
    87  		// probably deal with this by just matching version ranges and
    88  		// hard-coding the knowledge of which Terraform version uses which
    89  		// protocol version.
    90  		PluginProtocolVersion: plugin.Handshake.ProtocolVersion,
    91  
    92  		OS:   osName,
    93  		Arch: archName,
    94  	}
    95  
    96  	for name, constraints := range config.Providers {
    97  		c.ui.Info(fmt.Sprintf("Fetching provider %q...", name))
    98  		for _, constraint := range constraints {
    99  			meta, err := installer.Get(name, constraint.MustParse())
   100  			if err != nil {
   101  				c.ui.Error(fmt.Sprintf("Failed to resolve %s provider %s: %s", name, constraint, err))
   102  				return 1
   103  			}
   104  
   105  			c.ui.Info(fmt.Sprintf("- %q resolved to %s", constraint, meta.Version))
   106  		}
   107  	}
   108  
   109  	files, err := ioutil.ReadDir(workDir)
   110  	if err != nil {
   111  		c.ui.Error(fmt.Sprintf("Failed to read work directory %s: %s", workDir, err))
   112  		return 1
   113  	}
   114  
   115  	// If we get this far then our workDir now contains the union of the
   116  	// contents of all the zip files we downloaded above. We can now create
   117  	// our output file.
   118  	outFn := c.bundleFilename(config.Terraform.Version, time.Now(), osName, archName)
   119  	c.ui.Info(fmt.Sprintf("Creating %s ...", outFn))
   120  	outF, err := os.OpenFile(outFn, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
   121  	if err != nil {
   122  		c.ui.Error(fmt.Sprintf("Failed to create %s: %s", outFn, err))
   123  		return 1
   124  	}
   125  	outZ := zip.NewWriter(outF)
   126  	defer func() {
   127  		err := outZ.Close()
   128  		if err != nil {
   129  			c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
   130  			os.Exit(1)
   131  		}
   132  		err = outF.Close()
   133  		if err != nil {
   134  			c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
   135  			os.Exit(1)
   136  		}
   137  	}()
   138  
   139  	for _, file := range files {
   140  		if file.IsDir() {
   141  			// should never happen unless something tampers with our tmpdir
   142  			continue
   143  		}
   144  
   145  		fn := filepath.Join(workDir, file.Name())
   146  		r, err := os.Open(fn)
   147  		if err != nil {
   148  			c.ui.Error(fmt.Sprintf("Failed to open %s: %s", fn, err))
   149  			return 1
   150  		}
   151  		hdr, err := zip.FileInfoHeader(file)
   152  		if err != nil {
   153  			c.ui.Error(fmt.Sprintf("Failed to add zip entry for %s: %s", fn, err))
   154  			return 1
   155  		}
   156  		w, err := outZ.CreateHeader(hdr)
   157  		if err != nil {
   158  			c.ui.Error(fmt.Sprintf("Failed to add zip entry for %s: %s", fn, err))
   159  			return 1
   160  		}
   161  		_, err = io.Copy(w, r)
   162  		if err != nil {
   163  			c.ui.Error(fmt.Sprintf("Failed to write %s to bundle: %s", fn, err))
   164  			return 1
   165  		}
   166  	}
   167  
   168  	c.ui.Info("All done!")
   169  
   170  	return 0
   171  }
   172  
   173  func (c *PackageCommand) bundleFilename(version discovery.VersionStr, time time.Time, osName, archName string) string {
   174  	time = time.UTC()
   175  	return fmt.Sprintf(
   176  		"terraform_%s-bundle%04d%02d%02d%02d_%s_%s.zip",
   177  		version,
   178  		time.Year(), time.Month(), time.Day(), time.Hour(),
   179  		osName, archName,
   180  	)
   181  }
   182  
   183  func (c *PackageCommand) coreURL(version discovery.VersionStr, osName, archName string) string {
   184  	return fmt.Sprintf(
   185  		"%s/terraform/%s/terraform_%s_%s_%s.zip",
   186  		releasesBaseURL, version, version, osName, archName,
   187  	)
   188  }
   189  
   190  func (c *PackageCommand) Synopsis() string {
   191  	return "Produces a bundle archive"
   192  }
   193  
   194  func (c *PackageCommand) Help() string {
   195  	return `Usage: terraform-bundle package [options] <config-file>
   196  
   197  Uses the given bundle configuration file to produce a zip file in the
   198  current working directory containing a Terraform binary along with zero or
   199  more provider plugin binaries.
   200  
   201  Options:
   202    -os=name    Target operating system the archive will be built for. Defaults
   203                to that of the system where the command is being run.
   204  
   205    -arch=name  Target CPU architecture the archive will be built for. Defaults
   206                to that of the system where the command is being run.
   207  
   208  The resulting zip file can be used to more easily install Terraform and
   209  a fixed set of providers together on a server, so that Terraform's provider
   210  auto-installation mechanism can be avoided.
   211  
   212  To build an archive for Terraform Enterprise, use:
   213    -os=linux -arch=amd64
   214  
   215  Note that the given configuration file is a format specific to this command,
   216  not a normal Terraform configuration file. The file format looks like this:
   217  
   218    terraform {
   219      # Version of Terraform to include in the bundle. An exact version number
   220  	# is required.
   221      version = "0.10.0"
   222    }
   223  
   224    # Define which provider plugins are to be included
   225    providers {
   226      # Include the newest "aws" provider version in the 1.0 series.
   227      aws = ["~> 1.0"]
   228  
   229      # Include both the newest 1.0 and 2.0 versions of the "google" provider.
   230      # Each item in these lists allows a distinct version to be added. If the
   231  	# two expressions match different versions then _both_ are included in
   232  	# the bundle archive.
   233      google = ["~> 1.0", "~> 2.0"]
   234    }
   235  
   236  `
   237  }