~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v5-unstable/internal/charmstore/migrations_dump_test.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
package charmstore
 
2
 
 
3
import (
 
4
        "archive/zip"
 
5
        "bufio"
 
6
        "bytes"
 
7
        "encoding/binary"
 
8
        "encoding/json"
 
9
        "fmt"
 
10
        "go/build"
 
11
        "io/ioutil"
 
12
        "net/http"
 
13
        "os"
 
14
        "os/exec"
 
15
        "path"
 
16
        "path/filepath"
 
17
        "sort"
 
18
        "strings"
 
19
        "time"
 
20
 
 
21
        jujutesting "github.com/juju/testing"
 
22
        "github.com/juju/utils/fs"
 
23
        "gopkg.in/errgo.v1"
 
24
        "gopkg.in/macaroon-bakery.v1/bakery"
 
25
        "gopkg.in/mgo.v2"
 
26
        "gopkg.in/mgo.v2/bson"
 
27
        "gopkg.in/tomb.v2"
 
28
        "gopkg.in/yaml.v2"
 
29
 
 
30
        "gopkg.in/juju/charmstore.v5-unstable/config"
 
31
        "gopkg.in/juju/charmstore.v5-unstable/internal/blobstore"
 
32
)
 
33
 
 
34
// historicalDBName holds the name of the juju database
 
35
// as hard-coded in previous versions of the charm store server.
 
36
const historicalDBName = "juju"
 
37
 
 
38
// dumpMigrationHistory checks out and runs the charmstore version held
 
39
// in each element of history in sequence, runs any associated updates,
 
40
// and, if the version is not before earlierDeployedVersion, dumps
 
41
// the database to a file.
 
42
//
 
43
// After dumpMigrationHistory has been called, createDatabaseAtVersion
 
44
// can be used to backtrack the database to any of the dumped versions.
 
45
func dumpMigrationHistory(session *mgo.Session, earliestDeployedVersion string, history []versionSpec) error {
 
46
        db := session.DB(historicalDBName)
 
47
        vcsStatus, err := currentVCSStatus()
 
48
        if err != nil {
 
49
                return errgo.Mask(err)
 
50
        }
 
51
        dumping := false
 
52
        for _, vc := range history {
 
53
                logger.Infof("----------------- running version %v", vc.version)
 
54
                if vc.version == earliestDeployedVersion {
 
55
                        dumping = true
 
56
                }
 
57
                if err := runMigrationVersion(db, vc); err != nil {
 
58
                        return errgo.Notef(err, "cannot run at version %s", vc.version)
 
59
                }
 
60
                if dumping {
 
61
                        filename := migrationDumpFileName(vc.version)
 
62
                        logger.Infof("dumping database to %s", filename)
 
63
                        if err := saveDBToFile(db, vcsStatus, filename); err != nil {
 
64
                                return errgo.Notef(err, "cannot save DB at version %v", vc.version)
 
65
                        }
 
66
                }
 
67
        }
 
68
        if !dumping {
 
69
                return errgo.Newf("no versions matched earliest deployed version %q; nothing dumped", earliestDeployedVersion)
 
70
        }
 
71
        return nil
 
72
}
 
73
 
 
74
// createDatabaseAtVersion loads the database from the
 
75
// dump file for the given version (see dumpMigrationHistory).
 
76
func createDatabaseAtVersion(db *mgo.Database, version string) error {
 
77
        vcsStatus, err := restoreDBFromFile(db, migrationDumpFileName(version))
 
78
        if err != nil {
 
79
                return errgo.Notef(err, "cannot restore version %q", version)
 
80
        }
 
81
        logger.Infof("restored migration from version %s; dumped at %s", version, vcsStatus)
 
82
        return nil
 
83
}
 
84
 
 
85
// migrationDumpFileName returns the name of the file that
 
86
// the migration database snapshot will be saved to.
 
87
func migrationDumpFileName(version string) string {
 
88
        return "migrationdump." + version + ".zip"
 
89
}
 
90
 
 
91
// currentVCSStatus returns the git status of the current
 
92
// charmstore source code. This will be saved into the
 
93
// migration dump file so that there is some indication
 
94
// as to when that was created.
 
95
func currentVCSStatus() (string, error) {
 
96
        cmd := exec.Command("git", "describe")
 
97
        cmd.Stderr = os.Stderr
 
98
        data, err := cmd.Output()
 
99
        if err != nil {
 
100
                return "", errgo.Mask(err)
 
101
        }
 
102
        // With the --porcelain flag, git status prints a simple
 
103
        // line-per-locally-modified-file, or nothing at all if there
 
104
        // are no locally modified files.
 
105
        cmd = exec.Command("git", "status", "--porcelain")
 
106
        cmd.Stderr = os.Stderr
 
107
        data1, err := cmd.Output()
 
108
        if err != nil {
 
109
                return "", errgo.Mask(err)
 
110
        }
 
111
        return string(append(data, data1...)), nil
 
112
}
 
113
 
 
114
// saveDBToFile dumps the entire state of the database to the given
 
115
// file name, also saving the given VCS status.
 
116
func saveDBToFile(db *mgo.Database, vcsStatus string, filename string) (err error) {
 
117
        f, err := os.Create(filename)
 
118
        if err != nil {
 
119
                return errgo.Mask(err)
 
120
        }
 
121
        defer func() {
 
122
                if err != nil {
 
123
                        os.Remove(filename)
 
124
                }
 
125
        }()
 
126
        defer f.Close()
 
127
        zw := zip.NewWriter(f)
 
128
        defer func() {
 
129
                if err1 := zw.Close(); err1 != nil {
 
130
                        err = errgo.Notef(err1, "zip close failed")
 
131
                }
 
132
        }()
 
133
        collections, err := dumpDB(db)
 
134
        if err != nil {
 
135
                return errgo.Mask(err)
 
136
        }
 
137
        if err := writeVCSStatus(zw, vcsStatus); err != nil {
 
138
                return errgo.Mask(err)
 
139
        }
 
140
        for _, c := range collections {
 
141
                w, err := zw.Create(historicalDBName + "/" + c.name + ".bson")
 
142
                if err != nil {
 
143
                        return errgo.Mask(err)
 
144
                }
 
145
                if _, err := w.Write(c.data); err != nil {
 
146
                        return errgo.Mask(err)
 
147
                }
 
148
        }
 
149
        return nil
 
150
}
 
151
 
 
152
// restoreDBFromFile reads the database dump from the given file
 
153
// and restores it into db.
 
154
func restoreDBFromFile(db *mgo.Database, filename string) (vcsStatus string, _ error) {
 
155
        f, err := os.Open(filename)
 
156
        if err != nil {
 
157
                return "", errgo.Mask(err)
 
158
        }
 
159
        defer f.Close()
 
160
        info, err := f.Stat()
 
161
        if err != nil {
 
162
                return "", errgo.Mask(err)
 
163
        }
 
164
        zr, err := zip.NewReader(f, info.Size())
 
165
        if err != nil {
 
166
                return "", errgo.Mask(err)
 
167
        }
 
168
        var colls []collectionData
 
169
        for _, f := range zr.File {
 
170
                name := path.Clean(f.Name)
 
171
                if name == vcsStatusFile {
 
172
                        data, err := readZipFile(f)
 
173
                        if err != nil {
 
174
                                return "", errgo.Mask(err)
 
175
                        }
 
176
                        vcsStatus = string(data)
 
177
                        continue
 
178
                }
 
179
                if !strings.HasSuffix(name, ".bson") {
 
180
                        logger.Infof("ignoring %v", name)
 
181
                        continue
 
182
                }
 
183
                if !strings.HasPrefix(name, historicalDBName+"/") {
 
184
                        return "", errgo.Newf("file %s from unknown database found in dump file", name)
 
185
                }
 
186
                name = strings.TrimPrefix(name, historicalDBName+"/")
 
187
                name = strings.TrimSuffix(name, ".bson")
 
188
                data, err := readZipFile(f)
 
189
                if err != nil {
 
190
                        return "", errgo.Mask(err)
 
191
                }
 
192
                colls = append(colls, collectionData{
 
193
                        name: name,
 
194
                        data: data,
 
195
                })
 
196
        }
 
197
        if err := restoreDB(db, colls); err != nil {
 
198
                return "", errgo.Mask(err)
 
199
        }
 
200
        return vcsStatus, nil
 
201
}
 
202
 
 
203
// readZipFile reads the entire contents of f.
 
204
func readZipFile(f *zip.File) ([]byte, error) {
 
205
        r, err := f.Open()
 
206
        if err != nil {
 
207
                return nil, errgo.Mask(err)
 
208
        }
 
209
        defer r.Close()
 
210
        data, err := ioutil.ReadAll(r)
 
211
        if err != nil {
 
212
                return nil, errgo.Mask(err)
 
213
        }
 
214
        return data, nil
 
215
}
 
216
 
 
217
const vcsStatusFile = "vcs-status"
 
218
 
 
219
// writeVCSStatus writes the given VCS status into the
 
220
// given zip file.
 
221
func writeVCSStatus(zw *zip.Writer, vcsStatus string) error {
 
222
        w, err := zw.Create(vcsStatusFile)
 
223
        if err != nil {
 
224
                return errgo.Mask(err)
 
225
        }
 
226
        if _, err := w.Write([]byte(vcsStatus)); err != nil {
 
227
                return errgo.Mask(err)
 
228
        }
 
229
        return nil
 
230
}
 
231
 
 
232
const defaultCharmStoreRepo = "gopkg.in/juju/charmstore.v5-unstable"
 
233
 
 
234
// versionSpec specifies a version of the charm store to run
 
235
// and a function that will apply some updates to that
 
236
// version.
 
237
type versionSpec struct {
 
238
        version string
 
239
        // package holds the Go package containing the
 
240
        // charmd command. If empty, this defaults to
 
241
        //
 
242
        pkg string
 
243
        // update is called to apply updates after running charmd.
 
244
        update func(db *mgo.Database, csv *charmStoreVersion) error
 
245
}
 
246
 
 
247
var bogusPublicKey bakery.PublicKey
 
248
 
 
249
// runVersion runs the charm store at the given version
 
250
// and applies the associated updates.
 
251
func runMigrationVersion(db *mgo.Database, vc versionSpec) error {
 
252
        if vc.pkg == "" {
 
253
                vc.pkg = defaultCharmStoreRepo
 
254
        }
 
255
        csv, err := runCharmStoreVersion(vc.pkg, vc.version, &config.Config{
 
256
                MongoURL:          jujutesting.MgoServer.Addr(),
 
257
                AuthUsername:      "admin",
 
258
                AuthPassword:      "password",
 
259
                APIAddr:           fmt.Sprintf("localhost:%d", jujutesting.FindTCPPort()),
 
260
                MaxMgoSessions:    10,
 
261
                IdentityAPIURL:    "https://0.1.2.3/identity",
 
262
                IdentityPublicKey: &bogusPublicKey,
 
263
        })
 
264
        if err != nil {
 
265
                return errgo.Mask(err)
 
266
        }
 
267
        defer csv.Close()
 
268
        if vc.update == nil {
 
269
                return nil
 
270
        }
 
271
        if err := vc.update(db, csv); err != nil {
 
272
                return errgo.Notef(err, "cannot run update")
 
273
        }
 
274
        return nil
 
275
}
 
276
 
 
277
// collectionData holds all the dumped data from a collection.
 
278
type collectionData struct {
 
279
        // name holds the name of the collection.
 
280
        name string
 
281
        // data holds all the records from the collection as
 
282
        // a sequence of raw BSON records.
 
283
        data []byte
 
284
}
 
285
 
 
286
// dumpDB returns dumped data for all the non-system
 
287
// collections in the database.
 
288
func dumpDB(db *mgo.Database) ([]collectionData, error) {
 
289
        collections, err := db.CollectionNames()
 
290
        if err != nil {
 
291
                return nil, errgo.Mask(err)
 
292
        }
 
293
        sort.Strings(collections)
 
294
        var dumped []collectionData
 
295
        for _, c := range collections {
 
296
                if strings.HasPrefix(c, "system.") {
 
297
                        continue
 
298
                }
 
299
                data, err := dumpCollection(db.C(c))
 
300
                if err != nil {
 
301
                        return nil, errgo.Notef(err, "cannot dump %q: %v", c)
 
302
                }
 
303
                dumped = append(dumped, collectionData{
 
304
                        name: c,
 
305
                        data: data,
 
306
                })
 
307
        }
 
308
        return dumped, nil
 
309
}
 
310
 
 
311
// dumpCollection returns dumped data from a collection.
 
312
func dumpCollection(c *mgo.Collection) ([]byte, error) {
 
313
        var buf bytes.Buffer
 
314
        iter := c.Find(nil).Iter()
 
315
        var item bson.Raw
 
316
        for iter.Next(&item) {
 
317
                if item.Kind != 3 {
 
318
                        return nil, errgo.Newf("unexpected item kind in collection %v", item.Kind)
 
319
                }
 
320
                buf.Write(item.Data)
 
321
        }
 
322
        if err := iter.Err(); err != nil {
 
323
                return nil, errgo.Mask(err)
 
324
        }
 
325
        return buf.Bytes(), nil
 
326
}
 
327
 
 
328
// restoreDB restores all the given collections into the database.
 
329
func restoreDB(db *mgo.Database, dump []collectionData) error {
 
330
        if err := db.DropDatabase(); err != nil {
 
331
                return errgo.Notef(err, "cannot drop database %v", db.Name)
 
332
        }
 
333
        for _, cd := range dump {
 
334
                if err := restoreCollection(db.C(cd.name), cd.data); err != nil {
 
335
                        return errgo.Mask(err)
 
336
                }
 
337
        }
 
338
        return nil
 
339
}
 
340
 
 
341
// restoreCollection restores all the given data (in raw BSON format)
 
342
// into the given collection, dropping it first.
 
343
func restoreCollection(c *mgo.Collection, data []byte) error {
 
344
        if len(data) == 0 {
 
345
                return c.Create(&mgo.CollectionInfo{})
 
346
        }
 
347
        for len(data) > 0 {
 
348
                doc, rest := nextBSONDoc(data)
 
349
                data = rest
 
350
                if err := c.Insert(doc); err != nil {
 
351
                        return errgo.Mask(err)
 
352
                }
 
353
        }
 
354
        return nil
 
355
}
 
356
 
 
357
// nextBSONDoc returns the next BSON document from
 
358
// the given data, and the data following it.
 
359
func nextBSONDoc(data []byte) (bson.Raw, []byte) {
 
360
        if len(data) < 4 {
 
361
                panic("truncated record")
 
362
        }
 
363
        n := binary.LittleEndian.Uint32(data)
 
364
        return bson.Raw{
 
365
                Kind: 3,
 
366
                Data: data[0:n],
 
367
        }, data[n:]
 
368
}
 
369
 
 
370
// charmStoreVersion represents a specific checked-out
 
371
// version of the charm store code and a running version
 
372
// of its associated charmd command.
 
373
type charmStoreVersion struct {
 
374
        tomb tomb.Tomb
 
375
 
 
376
        // rootDir holds the root of the GOPATH directory
 
377
        // holding all the charmstore source.
 
378
        // This is copied from the GOPATH directory
 
379
        // that the charmstore tests are being run in.
 
380
        rootDir string
 
381
 
 
382
        // csAddr holds the address that can be used to
 
383
        // dial the running charmd.
 
384
        csAddr string
 
385
 
 
386
        // runningCmd refers to the running charmd, so that
 
387
        // it can be killed.
 
388
        runningCmd *exec.Cmd
 
389
}
 
390
 
 
391
// runCharmStoreVersion runs the given charm store version
 
392
// from the given repository Go path and starting it with
 
393
// the given configuration.
 
394
func runCharmStoreVersion(csRepo, version string, cfg *config.Config) (_ *charmStoreVersion, err error) {
 
395
        dir, err := ioutil.TempDir("", "charmstore-test")
 
396
        if err != nil {
 
397
                return nil, errgo.Mask(err)
 
398
        }
 
399
        defer func() {
 
400
                if err != nil {
 
401
                        os.RemoveAll(dir)
 
402
                }
 
403
        }()
 
404
        csv := &charmStoreVersion{
 
405
                rootDir: dir,
 
406
                csAddr:  cfg.APIAddr,
 
407
        }
 
408
        if err := csv.copyRepo(csRepo); err != nil {
 
409
                return nil, errgo.Mask(err)
 
410
        }
 
411
        destPkgDir := filepath.Join(csv.srcDir(), filepath.FromSlash(csRepo))
 
412
 
 
413
        // Discard any changes made in the local repo.
 
414
        if err := csv.runCmd(destPkgDir, "git", "reset", "--hard", "HEAD"); err != nil {
 
415
                return nil, errgo.Mask(err)
 
416
        }
 
417
 
 
418
        if err := csv.runCmd(destPkgDir, "git", "checkout", version); err != nil {
 
419
                return nil, errgo.Mask(err)
 
420
        }
 
421
        depFile := filepath.Join(destPkgDir, "dependencies.tsv")
 
422
        if err := csv.copyDeps(depFile); err != nil {
 
423
                return nil, errgo.Mask(err)
 
424
        }
 
425
        if err := csv.runCmd(destPkgDir, "godeps", "-force-clean", "-u", depFile); err != nil {
 
426
                return nil, errgo.Mask(err)
 
427
        }
 
428
        if err := csv.runCmd(destPkgDir, "go", "install", path.Join(csRepo, "/cmd/charmd")); err != nil {
 
429
                return nil, errgo.Mask(err)
 
430
        }
 
431
        if err := csv.startCS(cfg); err != nil {
 
432
                return nil, errgo.Mask(err)
 
433
        }
 
434
        return csv, nil
 
435
}
 
436
 
 
437
// srvDir returns the package root of the charm store source.
 
438
func (csv *charmStoreVersion) srcDir() string {
 
439
        return filepath.Join(csv.rootDir, "src")
 
440
}
 
441
 
 
442
// Close kills the charmd and removes all its associated files.
 
443
func (csv *charmStoreVersion) Close() error {
 
444
        csv.Kill()
 
445
        if err := csv.Wait(); err != nil {
 
446
                logger.Infof("warning: error closing down server: %#v", err)
 
447
        }
 
448
        return csv.remove()
 
449
}
 
450
 
 
451
// remove removes all the files associated with csv.
 
452
func (csv *charmStoreVersion) remove() error {
 
453
        return os.RemoveAll(csv.rootDir)
 
454
}
 
455
 
 
456
// uploadSpec specifies a entity to be uploaded through
 
457
// the API.
 
458
type uploadSpec struct {
 
459
        // usePost specifies that POST should be used rather than PUT.
 
460
        usePost bool
 
461
        // entity holds the entity to be uploaded.
 
462
        entity ArchiverTo
 
463
        // id holds the charm id to be uploaded to.
 
464
        id string
 
465
        // promulgatedId holds the promulgated id to be used,
 
466
        // valid only when usePost is false.
 
467
        promulgatedId string
 
468
}
 
469
 
 
470
// Upload uploads all the given entities to the charm store,
 
471
// using the given API version.
 
472
func (csv *charmStoreVersion) Upload(apiVersion string, specs []uploadSpec) error {
 
473
        for _, spec := range specs {
 
474
                if spec.usePost {
 
475
                        if err := csv.uploadWithPost(apiVersion, spec.entity, spec.id); err != nil {
 
476
                                return errgo.Mask(err)
 
477
                        }
 
478
                } else {
 
479
                        if err := csv.uploadWithPut(apiVersion, spec.entity, spec.id, spec.promulgatedId); err != nil {
 
480
                                return errgo.Mask(err)
 
481
                        }
 
482
                }
 
483
        }
 
484
        return nil
 
485
}
 
486
 
 
487
func (csv *charmStoreVersion) uploadWithPost(apiVersion string, entity ArchiverTo, url string) error {
 
488
        var buf bytes.Buffer
 
489
        if err := entity.ArchiveTo(&buf); err != nil {
 
490
                return errgo.Mask(err)
 
491
        }
 
492
        hash := blobstore.NewHash()
 
493
        hash.Write(buf.Bytes())
 
494
        logger.Infof("archive %d bytes", len(buf.Bytes()))
 
495
        req, err := http.NewRequest("POST", fmt.Sprintf("/%s/%s/archive?hash=%x", apiVersion, url, hash.Sum(nil)), &buf)
 
496
        if err != nil {
 
497
                return errgo.Mask(err)
 
498
        }
 
499
        req.Header.Set("Content-Type", "application/zip")
 
500
        resp, err := csv.DoRequest(req)
 
501
        if err != nil {
 
502
                return errgo.Mask(err)
 
503
        }
 
504
        defer resp.Body.Close()
 
505
        if resp.StatusCode != http.StatusOK {
 
506
                body, _ := ioutil.ReadAll(resp.Body)
 
507
                return errgo.Newf("unexpected response to POST %q: %v (body %q)", req.URL, resp.Status, body)
 
508
        }
 
509
        return nil
 
510
}
 
511
 
 
512
func (csv *charmStoreVersion) uploadWithPut(apiVersion string, entity ArchiverTo, url, promulgatedURL string) error {
 
513
        var buf bytes.Buffer
 
514
        if err := entity.ArchiveTo(&buf); err != nil {
 
515
                return errgo.Mask(err)
 
516
        }
 
517
        promulgatedParam := ""
 
518
        if promulgatedURL != "" {
 
519
                promulgatedParam = fmt.Sprintf("&promulgated=%s", promulgatedURL)
 
520
        }
 
521
        hash := blobstore.NewHash()
 
522
        hash.Write(buf.Bytes())
 
523
        logger.Infof("archive %d bytes", len(buf.Bytes()))
 
524
        req, err := http.NewRequest("PUT", fmt.Sprintf("/%s/%s/archive?hash=%x%s", apiVersion, url, hash.Sum(nil), promulgatedParam), &buf)
 
525
        if err != nil {
 
526
                return errgo.Mask(err)
 
527
        }
 
528
        req.Header.Set("Content-Type", "application/zip")
 
529
        resp, err := csv.DoRequest(req)
 
530
        if err != nil {
 
531
                return errgo.Mask(err)
 
532
        }
 
533
        defer resp.Body.Close()
 
534
        if resp.StatusCode != http.StatusOK {
 
535
                body, _ := ioutil.ReadAll(resp.Body)
 
536
                return errgo.Newf("unexpected response to PUT %q: %v (body %q)", req.URL, resp.Status, body)
 
537
        }
 
538
        return nil
 
539
}
 
540
 
 
541
// Put makes a PUT request containing the given body, JSON encoded, to the API.
 
542
// The urlPath parameter should contain only the URL path, not the host or scheme.
 
543
func (csv *charmStoreVersion) Put(urlPath string, body interface{}) error {
 
544
        data, err := json.Marshal(body)
 
545
        if err != nil {
 
546
                return errgo.Mask(err)
 
547
        }
 
548
        req, err := http.NewRequest("PUT", urlPath, bytes.NewReader(data))
 
549
        if err != nil {
 
550
                return errgo.Mask(err)
 
551
        }
 
552
        req.Header.Set("Content-Type", "application/json")
 
553
        resp, err := csv.DoRequest(req)
 
554
        if err != nil {
 
555
                return errgo.Mask(err)
 
556
        }
 
557
        defer resp.Body.Close()
 
558
        if resp.StatusCode != http.StatusOK {
 
559
                body, _ := ioutil.ReadAll(resp.Body)
 
560
                return errgo.Newf("unexpected response to PUT %q: %v (body %q)", req.URL, resp.Status, body)
 
561
        }
 
562
        return nil
 
563
}
 
564
 
 
565
// DoRequest sends the given HTTP request to the charm store server.
 
566
func (csv *charmStoreVersion) DoRequest(req *http.Request) (*http.Response, error) {
 
567
        req.SetBasicAuth("admin", "password")
 
568
        req.URL.Host = csv.csAddr
 
569
        req.URL.Scheme = "http"
 
570
        return http.DefaultClient.Do(req)
 
571
}
 
572
 
 
573
// waitUntilServerIsUp waits until the charmstore server is up.
 
574
// It returns an error if it has to wait longer than the given timeout.
 
575
func (csv *charmStoreVersion) waitUntilServerIsUp(timeout time.Duration) error {
 
576
        endt := time.Now().Add(timeout)
 
577
        for {
 
578
                req, err := http.NewRequest("GET", "/", nil)
 
579
                if err != nil {
 
580
                        return errgo.Mask(err)
 
581
                }
 
582
                resp, err := csv.DoRequest(req)
 
583
                if err == nil {
 
584
                        resp.Body.Close()
 
585
                        return nil
 
586
                }
 
587
                if time.Now().After(endt) {
 
588
                        return errgo.Notef(err, "timed out waiting for server to come up")
 
589
                }
 
590
                time.Sleep(100 * time.Millisecond)
 
591
        }
 
592
 
 
593
}
 
594
 
 
595
// startCS starts the charmd process running.
 
596
func (csv *charmStoreVersion) startCS(cfg *config.Config) error {
 
597
        data, err := yaml.Marshal(cfg)
 
598
        if err != nil {
 
599
                return errgo.Mask(err)
 
600
        }
 
601
        cfgPath := filepath.Join(csv.rootDir, "csconfig.yaml")
 
602
        if err := ioutil.WriteFile(cfgPath, data, 0666); err != nil {
 
603
                return errgo.Mask(err)
 
604
        }
 
605
        cmd := exec.Command(filepath.Join(csv.rootDir, "bin", "charmd"), "--logging-config=INFO", cfgPath)
 
606
        cmd.Stdout = os.Stdout
 
607
        cmd.Stderr = os.Stderr
 
608
        cmd.Dir = csv.rootDir
 
609
        if err := cmd.Start(); err != nil {
 
610
                return errgo.Mask(err)
 
611
        }
 
612
        csv.runningCmd = cmd
 
613
        csv.tomb.Go(func() error {
 
614
                return errgo.Mask(cmd.Wait())
 
615
        })
 
616
        if err := csv.waitUntilServerIsUp(10 * time.Second); err != nil {
 
617
                return errgo.Mask(err)
 
618
        }
 
619
        return nil
 
620
}
 
621
 
 
622
// Kill kills the charmstore server.
 
623
func (csv *charmStoreVersion) Kill() {
 
624
        csv.runningCmd.Process.Kill()
 
625
}
 
626
 
 
627
// Wait waits for the charmstore server to exit.
 
628
func (csv *charmStoreVersion) Wait() error {
 
629
        return csv.tomb.Wait()
 
630
}
 
631
 
 
632
// runCmd runs the given command in the given current
 
633
// working directory.
 
634
func (csv *charmStoreVersion) runCmd(cwd string, c string, arg ...string) error {
 
635
        logger.Infof("cd %v; %v %v", cwd, c, strings.Join(arg, " "))
 
636
        cmd := exec.Command(c, arg...)
 
637
        cmd.Env = envWithVars(map[string]string{
 
638
                "GOPATH": csv.rootDir,
 
639
        })
 
640
        cmd.Stdout = os.Stdout
 
641
        cmd.Stderr = os.Stderr
 
642
        cmd.Dir = cwd
 
643
        if err := cmd.Run(); err != nil {
 
644
                return errgo.Notef(err, "failed to run %v %v", c, arg)
 
645
        }
 
646
        return nil
 
647
}
 
648
 
 
649
// envWithVars returns the OS environment variables
 
650
// with the specified variables changed to their associated
 
651
// values.
 
652
func envWithVars(vars map[string]string) []string {
 
653
        env := os.Environ()
 
654
        for i, v := range env {
 
655
                j := strings.Index(v, "=")
 
656
                if j == -1 {
 
657
                        continue
 
658
                }
 
659
                name := v[0:j]
 
660
                if val, ok := vars[name]; ok {
 
661
                        env[i] = name + "=" + val
 
662
                        delete(vars, name)
 
663
                }
 
664
        }
 
665
        for name, val := range vars {
 
666
                env = append(env, name+"="+val)
 
667
        }
 
668
        return env
 
669
}
 
670
 
 
671
// copyDeps copies all the dependencies found in the godeps
 
672
// file depFile from the local version into csv.rootDir.
 
673
func (csv *charmStoreVersion) copyDeps(depFile string) error {
 
674
        f, err := os.Open(depFile)
 
675
        if err != nil {
 
676
                return errgo.Mask(err)
 
677
        }
 
678
        defer f.Close()
 
679
        for scan := bufio.NewScanner(f); scan.Scan(); {
 
680
                line := scan.Text()
 
681
                tabIndex := strings.Index(line, "\t")
 
682
                if tabIndex == -1 {
 
683
                        return errgo.Newf("no tab found in dependencies line %q", line)
 
684
                }
 
685
                pkgPath := line[0:tabIndex]
 
686
                if err := csv.copyRepo(pkgPath); err != nil {
 
687
                        return errgo.Mask(err)
 
688
                }
 
689
        }
 
690
        return nil
 
691
}
 
692
 
 
693
// copyRepo copies all the files inside the given importPath
 
694
// from their local version into csv.rootDir.
 
695
func (csv *charmStoreVersion) copyRepo(importPath string) error {
 
696
        pkg, err := build.Import(importPath, ".", build.FindOnly)
 
697
        if pkg.Dir == "" {
 
698
                return errgo.Mask(err)
 
699
        }
 
700
        destDir := filepath.Join(csv.srcDir(), filepath.FromSlash(pkg.ImportPath))
 
701
        if err := os.MkdirAll(filepath.Dir(destDir), 0777); err != nil {
 
702
                return errgo.Mask(err)
 
703
        }
 
704
        if err := fs.Copy(pkg.Dir, destDir); err != nil {
 
705
                return errgo.Mask(err)
 
706
        }
 
707
        return nil
 
708
}