github.com/gtmtechltd/terraform@v0.11.12-beta1/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  	discovery "github.com/hashicorp/terraform/plugin/discovery"
    19  	"github.com/mitchellh/cli"
    20  )
    21  
    22  type PackageCommand struct {
    23  	ui cli.Ui
    24  }
    25  
    26  // shameless stackoverflow copy + pasta https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
    27  func CopyFile(src, dst string) (err error) {
    28  	sfi, err := os.Stat(src)
    29  	if err != nil {
    30  		return
    31  	}
    32  	if !sfi.Mode().IsRegular() {
    33  		// cannot copy non-regular files (e.g., directories,
    34  		// symlinks, devices, etc.)
    35  		return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
    36  	}
    37  	dfi, err := os.Stat(dst)
    38  	if err != nil {
    39  		if !os.IsNotExist(err) {
    40  			return
    41  		}
    42  	} else {
    43  		if !(dfi.Mode().IsRegular()) {
    44  			return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
    45  		}
    46  		if os.SameFile(sfi, dfi) {
    47  			return
    48  		}
    49  	}
    50  	if err = os.Link(src, dst); err == nil {
    51  		return
    52  	}
    53  	err = copyFileContents(src, dst)
    54  	os.Chmod(dst, sfi.Mode())
    55  	return
    56  }
    57  
    58  // see above
    59  func copyFileContents(src, dst string) (err error) {
    60  	in, err := os.Open(src)
    61  	if err != nil {
    62  		return
    63  	}
    64  	defer in.Close()
    65  	out, err := os.Create(dst)
    66  	if err != nil {
    67  		return
    68  	}
    69  	defer func() {
    70  		cerr := out.Close()
    71  		if err == nil {
    72  			err = cerr
    73  		}
    74  	}()
    75  	if _, err = io.Copy(out, in); err != nil {
    76  		return
    77  	}
    78  	err = out.Sync()
    79  	return
    80  }
    81  
    82  func (c *PackageCommand) Run(args []string) int {
    83  	flags := flag.NewFlagSet("package", flag.ExitOnError)
    84  	osPtr := flags.String("os", "", "Target operating system")
    85  	archPtr := flags.String("arch", "", "Target CPU architecture")
    86  	pluginDirPtr := flags.String("plugin-dir", "", "Path to custom plugins directory")
    87  	err := flags.Parse(args)
    88  	if err != nil {
    89  		c.ui.Error(err.Error())
    90  		return 1
    91  	}
    92  
    93  	osName := runtime.GOOS
    94  	archName := runtime.GOARCH
    95  	pluginDir := "./plugins"
    96  	if *osPtr != "" {
    97  		osName = *osPtr
    98  	}
    99  	if *archPtr != "" {
   100  		archName = *archPtr
   101  	}
   102  	if *pluginDirPtr != "" {
   103  		pluginDir = *pluginDirPtr
   104  	}
   105  
   106  	if flags.NArg() != 1 {
   107  		c.ui.Error("Configuration filename is required")
   108  		return 1
   109  	}
   110  	configFn := flags.Arg(0)
   111  
   112  	config, err := LoadConfigFile(configFn)
   113  	if err != nil {
   114  		c.ui.Error(fmt.Sprintf("Failed to read config: %s", err))
   115  		return 1
   116  	}
   117  
   118  	if discovery.ConstraintStr("< 0.10.0-beta1").MustParse().Allows(config.Terraform.Version.MustParse()) {
   119  		c.ui.Error("Bundles can be created only for Terraform 0.10 or newer")
   120  		return 1
   121  	}
   122  
   123  	workDir, err := ioutil.TempDir("", "terraform-bundle")
   124  	if err != nil {
   125  		c.ui.Error(fmt.Sprintf("Could not create temporary dir: %s", err))
   126  		return 1
   127  	}
   128  	defer os.RemoveAll(workDir)
   129  
   130  	c.ui.Info(fmt.Sprintf("Fetching Terraform %s core package...", config.Terraform.Version))
   131  
   132  	coreZipURL := c.coreURL(config.Terraform.Version, osName, archName)
   133  	err = getter.Get(workDir, coreZipURL)
   134  
   135  	if err != nil {
   136  		c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err))
   137  	}
   138  
   139  	c.ui.Info(fmt.Sprintf("Fetching 3rd party plugins in directory: %s", pluginDir))
   140  	dirs := []string{pluginDir} //FindPlugins requires an array
   141  	localPlugins := discovery.FindPlugins("provider", dirs)
   142  	for k, _ := range localPlugins {
   143  		c.ui.Info(fmt.Sprintf("plugin: %s (%s)", k.Name, k.Version))
   144  	}
   145  	installer := &discovery.ProviderInstaller{
   146  		Dir: workDir,
   147  
   148  		// FIXME: This is incorrect because it uses the protocol version of
   149  		// this tool, rather than of the Terraform binary we just downloaded.
   150  		// But we can't get this information from a Terraform binary, so
   151  		// we'll just ignore this for now as we only have one protocol version
   152  		// in play anyway. If a new protocol version shows up later we will
   153  		// probably deal with this by just matching version ranges and
   154  		// hard-coding the knowledge of which Terraform version uses which
   155  		// protocol version.
   156  		PluginProtocolVersion: plugin.Handshake.ProtocolVersion,
   157  
   158  		OS:   osName,
   159  		Arch: archName,
   160  		Ui:   c.ui,
   161  	}
   162  
   163  	for name, constraintStrs := range config.Providers {
   164  		for _, constraintStr := range constraintStrs {
   165  			c.ui.Output(fmt.Sprintf("- Resolving %q provider (%s)...",
   166  				name, constraintStr))
   167  			foundPlugins := discovery.PluginMetaSet{}
   168  			constraint := constraintStr.MustParse()
   169  			for plugin, _ := range localPlugins {
   170  				if plugin.Name == name && constraint.Allows(plugin.Version.MustParse()) {
   171  					foundPlugins.Add(plugin)
   172  				}
   173  			}
   174  
   175  			if len(foundPlugins) > 0 {
   176  				plugin := foundPlugins.Newest()
   177  				CopyFile(plugin.Path, workDir+"/terraform-provider-"+plugin.Name+"_v"+plugin.Version.MustParse().String()) //put into temp dir
   178  			} else { //attempt to get from the public registry if not found locally
   179  				c.ui.Output(fmt.Sprintf("- Checking for provider plugin on %s...",
   180  					discovery.GetReleaseHost()))
   181  				_, err := installer.Get(name, constraint)
   182  				if err != nil {
   183  					c.ui.Error(fmt.Sprintf("- Failed to resolve %s provider %s: %s", name, constraint, err))
   184  					return 1
   185  				}
   186  			}
   187  		}
   188  	}
   189  
   190  	files, err := ioutil.ReadDir(workDir)
   191  	if err != nil {
   192  		c.ui.Error(fmt.Sprintf("Failed to read work directory %s: %s", workDir, err))
   193  		return 1
   194  	}
   195  
   196  	// If we get this far then our workDir now contains the union of the
   197  	// contents of all the zip files we downloaded above. We can now create
   198  	// our output file.
   199  	outFn := c.bundleFilename(config.Terraform.Version, time.Now(), osName, archName)
   200  	c.ui.Info(fmt.Sprintf("Creating %s ...", outFn))
   201  	outF, err := os.OpenFile(outFn, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
   202  	if err != nil {
   203  		c.ui.Error(fmt.Sprintf("Failed to create %s: %s", outFn, err))
   204  		return 1
   205  	}
   206  	outZ := zip.NewWriter(outF)
   207  	defer func() {
   208  		err := outZ.Close()
   209  		if err != nil {
   210  			c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
   211  			os.Exit(1)
   212  		}
   213  		err = outF.Close()
   214  		if err != nil {
   215  			c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
   216  			os.Exit(1)
   217  		}
   218  	}()
   219  
   220  	for _, file := range files {
   221  		if file.IsDir() {
   222  			// should never happen unless something tampers with our tmpdir
   223  			continue
   224  		}
   225  
   226  		fn := filepath.Join(workDir, file.Name())
   227  		r, err := os.Open(fn)
   228  		if err != nil {
   229  			c.ui.Error(fmt.Sprintf("Failed to open %s: %s", fn, err))
   230  			return 1
   231  		}
   232  		hdr, err := zip.FileInfoHeader(file)
   233  		if err != nil {
   234  			c.ui.Error(fmt.Sprintf("Failed to add zip entry for %s: %s", fn, err))
   235  			return 1
   236  		}
   237  		hdr.Method = zip.Deflate // be sure to compress files
   238  		w, err := outZ.CreateHeader(hdr)
   239  		if err != nil {
   240  			c.ui.Error(fmt.Sprintf("Failed to add zip entry for %s: %s", fn, err))
   241  			return 1
   242  		}
   243  		_, err = io.Copy(w, r)
   244  		if err != nil {
   245  			c.ui.Error(fmt.Sprintf("Failed to write %s to bundle: %s", fn, err))
   246  			return 1
   247  		}
   248  	}
   249  
   250  	c.ui.Info("All done!")
   251  
   252  	return 0
   253  }
   254  
   255  func (c *PackageCommand) bundleFilename(version discovery.VersionStr, time time.Time, osName, archName string) string {
   256  	time = time.UTC()
   257  	return fmt.Sprintf(
   258  		"terraform_%s-bundle%04d%02d%02d%02d_%s_%s.zip",
   259  		version,
   260  		time.Year(), time.Month(), time.Day(), time.Hour(),
   261  		osName, archName,
   262  	)
   263  }
   264  
   265  func (c *PackageCommand) coreURL(version discovery.VersionStr, osName, archName string) string {
   266  	return fmt.Sprintf(
   267  		"%s/terraform/%s/terraform_%s_%s_%s.zip",
   268  		discovery.GetReleaseHost(), version, version, osName, archName,
   269  	)
   270  }
   271  
   272  func (c *PackageCommand) Synopsis() string {
   273  	return "Produces a bundle archive"
   274  }
   275  
   276  func (c *PackageCommand) Help() string {
   277  	return `Usage: terraform-bundle package [options] <config-file>
   278  
   279  Uses the given bundle configuration file to produce a zip file in the
   280  current working directory containing a Terraform binary along with zero or
   281  more provider plugin binaries.
   282  
   283  Options:
   284    -os=name    		Target operating system the archive will be built for. Defaults
   285                		to that of the system where the command is being run.
   286  
   287    -arch=name  		Target CPU architecture the archive will be built for. Defaults
   288  					to that of the system where the command is being run.
   289  					  
   290    -plugin-dir=path 	The path to the custom plugins directory. Defaults to "./plugins".
   291  
   292  The resulting zip file can be used to more easily install Terraform and
   293  a fixed set of providers together on a server, so that Terraform's provider
   294  auto-installation mechanism can be avoided.
   295  
   296  To build an archive for Terraform Enterprise, use:
   297    -os=linux -arch=amd64
   298  
   299  Note that the given configuration file is a format specific to this command,
   300  not a normal Terraform configuration file. The file format looks like this:
   301  
   302    terraform {
   303      # Version of Terraform to include in the bundle. An exact version number
   304  	# is required.
   305      version = "0.10.0"
   306    }
   307  
   308    # Define which provider plugins are to be included
   309    providers {
   310      # Include the newest "aws" provider version in the 1.0 series.
   311      aws = ["~> 1.0"]
   312  
   313      # Include both the newest 1.0 and 2.0 versions of the "google" provider.
   314      # Each item in these lists allows a distinct version to be added. If the
   315  	# two expressions match different versions then _both_ are included in
   316  	# the bundle archive.
   317  	google = ["~> 1.0", "~> 2.0"]
   318  	
   319  	#Include a custom plugin to the bundle. Will search for the plugin in the 
   320  	#plugins directory, and package it with the bundle archive. Plugin must have
   321  	#a name of the form: terraform-provider-*-v*, and must be built with the operating
   322  	#system and architecture that terraform enterprise is running, e.g. linux and amd64
   323  	customplugin = ["0.1"]
   324    }
   325  
   326  `
   327  }