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 }