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 }