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 }