forked from hashicorp/terraform
/
package.go
325 lines (282 loc) · 9.17 KB
/
package.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
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"
discovery "github.com/hashicorp/terraform/plugin/discovery"
"github.com/mitchellh/cli"
)
type PackageCommand struct {
ui cli.Ui
}
// shameless stackoverflow copy + pasta https://stackoverflow.com/questions/21060945/simple-way-to-copy-a-file-in-golang
func CopyFile(src, dst string) (err error) {
sfi, err := os.Stat(src)
if err != nil {
return
}
if !sfi.Mode().IsRegular() {
// cannot copy non-regular files (e.g., directories,
// symlinks, devices, etc.)
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
}
dfi, err := os.Stat(dst)
if err != nil {
if !os.IsNotExist(err) {
return
}
} else {
if !(dfi.Mode().IsRegular()) {
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
}
if os.SameFile(sfi, dfi) {
return
}
}
if err = os.Link(src, dst); err == nil {
return
}
err = copyFileContents(src, dst)
return
}
// see above
func copyFileContents(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
return
}
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")
pluginDirPtr := flags.String("plugin-dir", "", "Path to custom plugins directory")
err := flags.Parse(args)
if err != nil {
c.ui.Error(err.Error())
return 1
}
osName := runtime.GOOS
archName := runtime.GOARCH
pluginDir := "./plugins"
if *osPtr != "" {
osName = *osPtr
}
if *archPtr != "" {
archName = *archPtr
}
if *pluginDirPtr != "" {
pluginDir = *pluginDirPtr
}
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))
}
c.ui.Info(fmt.Sprintf("Fetching 3rd party plugins in directory: %s", pluginDir))
dirs := []string{pluginDir} //FindPlugins requires an array
localPlugins := discovery.FindPlugins("provider", dirs)
for k, _ := range localPlugins {
c.ui.Info(fmt.Sprintf("plugin: %s (%s)", k.Name, k.Version))
}
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,
Ui: c.ui,
}
for name, constraintStrs := range config.Providers {
for _, constraintStr := range constraintStrs {
c.ui.Output(fmt.Sprintf("- Resolving %q provider (%s)...",
name, constraintStr))
foundPlugins := discovery.PluginMetaSet{}
constraint := constraintStr.MustParse()
for plugin, _ := range localPlugins {
if plugin.Name == name && constraint.Allows(plugin.Version.MustParse()) {
foundPlugins.Add(plugin)
}
}
if len(foundPlugins) > 0 {
plugin := foundPlugins.Newest()
CopyFile(plugin.Path, workDir+"/terraform-provider-"+plugin.Name+"-v"+plugin.Version.MustParse().String()) //put into temp dir
} else { //attempt to get from the public registry if not found locally
c.ui.Output(fmt.Sprintf("- Checking for provider plugin on %s...",
discovery.GetReleaseHost()))
_, err := installer.Get(name, constraint)
if err != nil {
c.ui.Error(fmt.Sprintf("- Failed to resolve %s provider %s: %s", name, constraint, err))
return 1
}
}
}
}
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",
discovery.GetReleaseHost(), 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.
-plugin-dir=path The path to the custom plugins directory. Defaults to "./plugins".
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"]
#Include a custom plugin to the bundle. Will search for the plugin in the
#plugins directory, and package it with the bundle archive. Plugin must have
#a name of the form: terraform-provider-*-v*, and must be built with the operating
#system and architecture that terraform enterprise is running, e.g. linux and amd64
customplugin = ["0.1"]
}
`
}