forked from hashicorp/packer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
push.go
331 lines (279 loc) · 7.64 KB
/
push.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
326
327
328
329
330
331
package command
import (
"flag"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"strings"
"github.com/hashicorp/atlas-go/archive"
"github.com/hashicorp/atlas-go/v1"
"github.com/mitchellh/packer/packer"
)
// archiveTemplateEntry is the name the template always takes within the slug.
const archiveTemplateEntry = ".packer-template"
type PushCommand struct {
Meta
client *atlas.Client
// For tests:
uploadFn pushUploadFn
}
// pushUploadFn is the callback type used for tests to stub out the uploading
// logic of the push command.
type pushUploadFn func(
io.Reader, *uploadOpts) (<-chan struct{}, <-chan error, error)
func (c *PushCommand) Run(args []string) int {
var create bool
var token string
f := flag.NewFlagSet("push", flag.ContinueOnError)
f.Usage = func() { c.Ui.Error(c.Help()) }
f.BoolVar(&create, "create", false, "create")
f.StringVar(&token, "token", "", "token")
if err := f.Parse(args); err != nil {
return 1
}
args = f.Args()
if len(args) != 1 {
f.Usage()
return 1
}
// Read the template
tpl, err := packer.ParseTemplateFile(args[0], nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
return 1
}
// Validate some things
if tpl.Push.Name == "" {
c.Ui.Error(fmt.Sprintf(
"The 'push' section must be specified in the template with\n" +
"at least the 'name' option set."))
return 1
}
// Determine our token
if token == "" {
token = tpl.Push.Token
}
// Build our client
defer func() { c.client = nil }()
c.client = atlas.DefaultClient()
if tpl.Push.Address != "" {
c.client, err = atlas.NewClient(tpl.Push.Address)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error setting up API client: %s", err))
return 1
}
}
if token != "" {
c.client.Token = token
}
// Build the archiving options
var opts archive.ArchiveOpts
opts.Include = tpl.Push.Include
opts.Exclude = tpl.Push.Exclude
opts.VCS = tpl.Push.VCS
opts.Extra = map[string]string{
archiveTemplateEntry: args[0],
}
// Determine the path we're archiving. This logic is a bit complicated
// as there are three possibilities:
//
// 1.) BaseDir is an absolute path, just use that.
//
// 2.) BaseDir is empty, so we use the directory of the template.
//
// 3.) BaseDir is relative, so we use the path relative to the directory
// of the template.
//
path := tpl.Push.BaseDir
if path == "" || !filepath.IsAbs(path) {
tplPath, err := filepath.Abs(args[0])
if err != nil {
c.Ui.Error(fmt.Sprintf("Error determining path to archive: %s", err))
return 1
}
tplPath = filepath.Dir(tplPath)
if path != "" {
tplPath = filepath.Join(tplPath, path)
}
path, err = filepath.Abs(tplPath)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error determining path to archive: %s", err))
return 1
}
}
// Find the Atlas post-processors, if possible
var atlasPPs []packer.RawPostProcessorConfig
for _, list := range tpl.PostProcessors {
for _, pp := range list {
if pp.Type == "atlas" {
atlasPPs = append(atlasPPs, pp)
}
}
}
// Build the upload options
var uploadOpts uploadOpts
uploadOpts.Slug = tpl.Push.Name
uploadOpts.Builds = make(map[string]*uploadBuildInfo)
for _, b := range tpl.Builders {
info := &uploadBuildInfo{Type: b.Type}
// Determine if we're artifacting this build
for _, pp := range atlasPPs {
if !pp.Skip(b.Name) {
info.Artifact = true
break
}
}
uploadOpts.Builds[b.Name] = info
}
// Warn about builds not having post-processors.
var badBuilds []string
for name, b := range uploadOpts.Builds {
if b.Artifact {
continue
}
badBuilds = append(badBuilds, name)
}
if len(badBuilds) > 0 {
c.Ui.Error(fmt.Sprintf(
"Warning! One or more of the builds in this template does not\n"+
"have an Atlas post-processor. Artifacts from this template will\n"+
"not appear in the Atlas artifact registry.\n\n"+
"This is just a warning. Atlas will still build your template\n"+
"and assume other post-processors are sending the artifacts where\n"+
"they need to go.\n\n"+
"Builds: %s\n\n", strings.Join(badBuilds, ", ")))
}
// Create the build config if it doesn't currently exist.
if err := c.create(uploadOpts.Slug, create); err != nil {
c.Ui.Error(err.Error())
return 1
}
// Start the archiving process
r, err := archive.CreateArchive(path, &opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error archiving: %s", err))
return 1
}
defer r.Close()
// Start the upload process
doneCh, uploadErrCh, err := c.upload(r, &uploadOpts)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error starting upload: %s", err))
return 1
}
// Make a ctrl-C channel
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
defer signal.Stop(sigCh)
err = nil
select {
case err = <-uploadErrCh:
err = fmt.Errorf("Error uploading: %s", err)
case <-sigCh:
err = fmt.Errorf("Push cancelled from Ctrl-C")
case <-doneCh:
}
if err != nil {
c.Ui.Error(err.Error())
return 1
}
c.Ui.Output(fmt.Sprintf("Push successful to '%s'", tpl.Push.Name))
return 0
}
func (*PushCommand) Help() string {
helpText := `
Usage: packer push [options] TEMPLATE
Push the template and the files it needs to a Packer build service.
This will not initiate any builds, it will only update the templates
used for builds.
The configuration about what is pushed is configured within the
template's "push" section.
Options:
-create Create the build configuration if it doesn't exist.
-token=<token> Access token to use to upload. If blank, the
ATLAS_TOKEN environmental variable will be used.
`
return strings.TrimSpace(helpText)
}
func (*PushCommand) Synopsis() string {
return "push template files to a Packer build service"
}
func (c *PushCommand) create(name string, create bool) error {
if c.uploadFn != nil {
return nil
}
// Separate the slug into the user and name components
user, name, err := atlas.ParseSlug(name)
if err != nil {
return fmt.Errorf("Malformed push name: %s", err)
}
// Check if it exists. If so, we're done.
if _, err := c.client.BuildConfig(user, name); err == nil {
return nil
} else if err != atlas.ErrNotFound {
return err
}
// Otherwise, show an error if we're not creating.
if !create {
return fmt.Errorf(
"Push target doesn't exist: %s. Either create this online via\n"+
"the website or pass the -create flag.", name)
}
// Create it
if err := c.client.CreateBuildConfig(user, name); err != nil {
return err
}
return nil
}
func (c *PushCommand) upload(
r *archive.Archive, opts *uploadOpts) (<-chan struct{}, <-chan error, error) {
if c.uploadFn != nil {
return c.uploadFn(r, opts)
}
// Separate the slug into the user and name components
user, name, err := atlas.ParseSlug(opts.Slug)
if err != nil {
return nil, nil, fmt.Errorf("upload: %s", err)
}
// Get the app
bc, err := c.client.BuildConfig(user, name)
if err != nil {
return nil, nil, fmt.Errorf("upload: %s", err)
}
// Build the version to send up
version := atlas.BuildConfigVersion{
User: bc.User,
Name: bc.Name,
Builds: make([]atlas.BuildConfigBuild, 0, len(opts.Builds)),
}
for name, info := range opts.Builds {
version.Builds = append(version.Builds, atlas.BuildConfigBuild{
Name: name,
Type: info.Type,
Artifact: info.Artifact,
})
}
// Start the upload
doneCh, errCh := make(chan struct{}), make(chan error)
go func() {
err := c.client.UploadBuildConfigVersion(&version, r, r.Size)
if err != nil {
errCh <- err
return
}
close(doneCh)
}()
return doneCh, errCh, nil
}
type uploadOpts struct {
URL string
Slug string
Builds map[string]*uploadBuildInfo
}
type uploadBuildInfo struct {
Type string
Artifact bool
}