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