terraform/tools/terraform-bundle/package.go

238 lines
6.6 KiB
Go
Raw Normal View History

terraform-bundle tool for bundling Terraform with providers Normally "terraform init" will download and install the plugins necessary to work with a particular configuration, but sometimes Terraform is deployed in a network that, for one reason or another, cannot access the official plugin repository for automatic download. terraform-bundle provides an alternative method, allowing the auto-download process to be run out-of-band on a separate machine that _does_ have access to the repository. The result is a zip file that can be extracted onto the target system to install both the desired Terraform version and a selection of providers, thus avoiding the need for on-the-fly plugin installation. This is provided as a separate tool from Terraform because it is not something that most users will need. In the rare case where this is needed, we will for the moment assume that users are able to build this tool themselves. We may later release it in a pre-built form, if it proves to be generally useful. It uses the same API from the plugin/discovery package is is used by the auto-install behavior in "terraform init", so plugin versions are resolved in the same way. However, it's expected that several different Terraform configurations will run from the same bundle, so this tool allows the bundle to include potentially many versions of the same provider and thus allows each Terraform configuration to select from the available versions in the bundle, avoiding the need to upgrade all configurations to new provider versions in lockstep.
2017-07-05 18:44:50 +02:00
package main
import (
"archive/zip"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"time"
"flag"
"io"
getter "github.com/hashicorp/go-getter"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/mitchellh/cli"
)
const releasesBaseURL = "https://releases.hashicorp.com"
type PackageCommand struct {
ui cli.Ui
}
func (c *PackageCommand) Run(args []string) int {
flags := flag.NewFlagSet("package", flag.ExitOnError)
osPtr := flags.String("os", "", "Target operating system")
archPtr := flags.String("arch", "", "Target CPU architecture")
err := flags.Parse(args)
if err != nil {
c.ui.Error(err.Error())
return 1
}
osName := runtime.GOOS
archName := runtime.GOARCH
if *osPtr != "" {
osName = *osPtr
}
if *archPtr != "" {
archName = *archPtr
}
if flags.NArg() != 1 {
c.ui.Error("Configuration filename is required")
return 1
}
configFn := flags.Arg(0)
config, err := LoadConfigFile(configFn)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to read config: %s", err))
return 1
}
if discovery.ConstraintStr("< 0.10.0-beta1").MustParse().Allows(config.Terraform.Version.MustParse()) {
c.ui.Error("Bundles can be created only for Terraform 0.10 or newer")
return 1
}
workDir, err := ioutil.TempDir("", "terraform-bundle")
if err != nil {
c.ui.Error(fmt.Sprintf("Could not create temporary dir: %s", err))
return 1
}
defer os.RemoveAll(workDir)
c.ui.Info(fmt.Sprintf("Fetching Terraform %s core package...", config.Terraform.Version))
coreZipURL := c.coreURL(config.Terraform.Version, osName, archName)
err = getter.Get(workDir, coreZipURL)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to fetch core package from %s: %s", coreZipURL, err))
}
installer := &discovery.ProviderInstaller{
Dir: workDir,
// FIXME: This is incorrect because it uses the protocol version of
// this tool, rather than of the Terraform binary we just downloaded.
// But we can't get this information from a Terraform binary, so
// we'll just ignore this for now as we only have one protocol version
// in play anyway. If a new protocol version shows up later we will
// probably deal with this by just matching version ranges and
// hard-coding the knowledge of which Terraform version uses which
// protocol version.
PluginProtocolVersion: plugin.Handshake.ProtocolVersion,
OS: osName,
Arch: archName,
}
for name, constraints := range config.Providers {
c.ui.Info(fmt.Sprintf("Fetching provider %q...", name))
for _, constraint := range constraints {
meta, err := installer.Get(name, constraint.MustParse())
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to resolve %s provider %s: %s", name, constraint, err))
return 1
}
c.ui.Info(fmt.Sprintf("- %q resolved to %s", constraint, meta.Version))
}
}
files, err := ioutil.ReadDir(workDir)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to read work directory %s: %s", workDir, err))
return 1
}
// If we get this far then our workDir now contains the union of the
// contents of all the zip files we downloaded above. We can now create
// our output file.
outFn := c.bundleFilename(config.Terraform.Version, time.Now(), osName, archName)
c.ui.Info(fmt.Sprintf("Creating %s ...", outFn))
outF, err := os.OpenFile(outFn, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to create %s: %s", outFn, err))
return 1
}
outZ := zip.NewWriter(outF)
defer func() {
err := outZ.Close()
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
os.Exit(1)
}
err = outF.Close()
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to close %s: %s", outFn, err))
os.Exit(1)
}
}()
for _, file := range files {
if file.IsDir() {
// should never happen unless something tampers with our tmpdir
continue
}
fn := filepath.Join(workDir, file.Name())
r, err := os.Open(fn)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to open %s: %s", fn, err))
return 1
}
hdr, err := zip.FileInfoHeader(file)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to add zip entry for %s: %s", fn, err))
return 1
}
w, err := outZ.CreateHeader(hdr)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to add zip entry for %s: %s", fn, err))
return 1
}
_, err = io.Copy(w, r)
if err != nil {
c.ui.Error(fmt.Sprintf("Failed to write %s to bundle: %s", fn, err))
return 1
}
}
c.ui.Info("All done!")
return 0
}
func (c *PackageCommand) bundleFilename(version discovery.VersionStr, time time.Time, osName, archName string) string {
time = time.UTC()
return fmt.Sprintf(
"terraform_%s-bundle%04d%02d%02d%02d_%s_%s.zip",
version,
time.Year(), time.Month(), time.Day(), time.Hour(),
osName, archName,
)
}
func (c *PackageCommand) coreURL(version discovery.VersionStr, osName, archName string) string {
return fmt.Sprintf(
"%s/terraform/%s/terraform_%s_%s_%s.zip",
releasesBaseURL, version, version, osName, archName,
)
}
func (c *PackageCommand) Synopsis() string {
return "Produces a bundle archive"
}
func (c *PackageCommand) Help() string {
return `Usage: terraform-bundle package [options] <config-file>
Uses the given bundle configuration file to produce a zip file in the
current working directory containing a Terraform binary along with zero or
more provider plugin binaries.
Options:
-os=name Target operating system the archive will be built for. Defaults
to that of the system where the command is being run.
-arch=name Target CPU architecture the archive will be built for. Defaults
to that of the system where the command is being run.
The resulting zip file can be used to more easily install Terraform and
a fixed set of providers together on a server, so that Terraform's provider
auto-installation mechanism can be avoided.
To build an archive for Terraform Enterprise, use:
-os=linux -arch=amd64
Note that the given configuration file is a format specific to this command,
not a normal Terraform configuration file. The file format looks like this:
terraform {
# Version of Terraform to include in the bundle. An exact version number
# is required.
version = "0.10.0"
}
# Define which provider plugins are to be included
providers {
# Include the newest "aws" provider version in the 1.0 series.
aws = ["~> 1.0"]
# Include both the newest 1.0 and 2.0 versions of the "google" provider.
# Each item in these lists allows a distinct version to be added. If the
# two expressions match different versions then _both_ are included in
# the bundle archive.
google = ["~> 1.0", "~> 2.0"]
}
`
}