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