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 }