21
jujutesting "github.com/juju/testing"
22
"github.com/juju/utils/fs"
24
"gopkg.in/macaroon-bakery.v1/bakery"
26
"gopkg.in/mgo.v2/bson"
30
"gopkg.in/juju/charmstore.v5-unstable/config"
31
"gopkg.in/juju/charmstore.v5-unstable/internal/blobstore"
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"
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.
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()
49
return errgo.Mask(err)
52
for _, vc := range history {
53
logger.Infof("----------------- running version %v", vc.version)
54
if vc.version == earliestDeployedVersion {
57
if err := runMigrationVersion(db, vc); err != nil {
58
return errgo.Notef(err, "cannot run at version %s", vc.version)
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)
69
return errgo.Newf("no versions matched earliest deployed version %q; nothing dumped", earliestDeployedVersion)
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))
79
return errgo.Notef(err, "cannot restore version %q", version)
81
logger.Infof("restored migration from version %s; dumped at %s", version, vcsStatus)
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"
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()
100
return "", errgo.Mask(err)
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()
109
return "", errgo.Mask(err)
111
return string(append(data, data1...)), nil
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)
119
return errgo.Mask(err)
127
zw := zip.NewWriter(f)
129
if err1 := zw.Close(); err1 != nil {
130
err = errgo.Notef(err1, "zip close failed")
133
collections, err := dumpDB(db)
135
return errgo.Mask(err)
137
if err := writeVCSStatus(zw, vcsStatus); err != nil {
138
return errgo.Mask(err)
140
for _, c := range collections {
141
w, err := zw.Create(historicalDBName + "/" + c.name + ".bson")
143
return errgo.Mask(err)
145
if _, err := w.Write(c.data); err != nil {
146
return errgo.Mask(err)
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)
157
return "", errgo.Mask(err)
160
info, err := f.Stat()
162
return "", errgo.Mask(err)
164
zr, err := zip.NewReader(f, info.Size())
166
return "", errgo.Mask(err)
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)
174
return "", errgo.Mask(err)
176
vcsStatus = string(data)
179
if !strings.HasSuffix(name, ".bson") {
180
logger.Infof("ignoring %v", name)
183
if !strings.HasPrefix(name, historicalDBName+"/") {
184
return "", errgo.Newf("file %s from unknown database found in dump file", name)
186
name = strings.TrimPrefix(name, historicalDBName+"/")
187
name = strings.TrimSuffix(name, ".bson")
188
data, err := readZipFile(f)
190
return "", errgo.Mask(err)
192
colls = append(colls, collectionData{
197
if err := restoreDB(db, colls); err != nil {
198
return "", errgo.Mask(err)
200
return vcsStatus, nil
203
// readZipFile reads the entire contents of f.
204
func readZipFile(f *zip.File) ([]byte, error) {
207
return nil, errgo.Mask(err)
210
data, err := ioutil.ReadAll(r)
212
return nil, errgo.Mask(err)
217
const vcsStatusFile = "vcs-status"
219
// writeVCSStatus writes the given VCS status into the
221
func writeVCSStatus(zw *zip.Writer, vcsStatus string) error {
222
w, err := zw.Create(vcsStatusFile)
224
return errgo.Mask(err)
226
if _, err := w.Write([]byte(vcsStatus)); err != nil {
227
return errgo.Mask(err)
232
const defaultCharmStoreRepo = "gopkg.in/juju/charmstore.v5-unstable"
234
// versionSpec specifies a version of the charm store to run
235
// and a function that will apply some updates to that
237
type versionSpec struct {
239
// package holds the Go package containing the
240
// charmd command. If empty, this defaults to
243
// update is called to apply updates after running charmd.
244
update func(db *mgo.Database, csv *charmStoreVersion) error
247
var bogusPublicKey bakery.PublicKey
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 {
253
vc.pkg = defaultCharmStoreRepo
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()),
261
IdentityAPIURL: "https://0.1.2.3/identity",
262
IdentityPublicKey: &bogusPublicKey,
265
return errgo.Mask(err)
268
if vc.update == nil {
271
if err := vc.update(db, csv); err != nil {
272
return errgo.Notef(err, "cannot run update")
277
// collectionData holds all the dumped data from a collection.
278
type collectionData struct {
279
// name holds the name of the collection.
281
// data holds all the records from the collection as
282
// a sequence of raw BSON records.
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()
291
return nil, errgo.Mask(err)
293
sort.Strings(collections)
294
var dumped []collectionData
295
for _, c := range collections {
296
if strings.HasPrefix(c, "system.") {
299
data, err := dumpCollection(db.C(c))
301
return nil, errgo.Notef(err, "cannot dump %q: %v", c)
303
dumped = append(dumped, collectionData{
311
// dumpCollection returns dumped data from a collection.
312
func dumpCollection(c *mgo.Collection) ([]byte, error) {
314
iter := c.Find(nil).Iter()
316
for iter.Next(&item) {
318
return nil, errgo.Newf("unexpected item kind in collection %v", item.Kind)
322
if err := iter.Err(); err != nil {
323
return nil, errgo.Mask(err)
325
return buf.Bytes(), nil
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)
333
for _, cd := range dump {
334
if err := restoreCollection(db.C(cd.name), cd.data); err != nil {
335
return errgo.Mask(err)
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 {
345
return c.Create(&mgo.CollectionInfo{})
348
doc, rest := nextBSONDoc(data)
350
if err := c.Insert(doc); err != nil {
351
return errgo.Mask(err)
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) {
361
panic("truncated record")
363
n := binary.LittleEndian.Uint32(data)
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 {
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.
382
// csAddr holds the address that can be used to
383
// dial the running charmd.
386
// runningCmd refers to the running charmd, so that
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")
397
return nil, errgo.Mask(err)
404
csv := &charmStoreVersion{
408
if err := csv.copyRepo(csRepo); err != nil {
409
return nil, errgo.Mask(err)
411
destPkgDir := filepath.Join(csv.srcDir(), filepath.FromSlash(csRepo))
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)
418
if err := csv.runCmd(destPkgDir, "git", "checkout", version); err != nil {
419
return nil, errgo.Mask(err)
421
depFile := filepath.Join(destPkgDir, "dependencies.tsv")
422
if err := csv.copyDeps(depFile); err != nil {
423
return nil, errgo.Mask(err)
425
if err := csv.runCmd(destPkgDir, "godeps", "-force-clean", "-u", depFile); err != nil {
426
return nil, errgo.Mask(err)
428
if err := csv.runCmd(destPkgDir, "go", "install", path.Join(csRepo, "/cmd/charmd")); err != nil {
429
return nil, errgo.Mask(err)
431
if err := csv.startCS(cfg); err != nil {
432
return nil, errgo.Mask(err)
437
// srvDir returns the package root of the charm store source.
438
func (csv *charmStoreVersion) srcDir() string {
439
return filepath.Join(csv.rootDir, "src")
442
// Close kills the charmd and removes all its associated files.
443
func (csv *charmStoreVersion) Close() error {
445
if err := csv.Wait(); err != nil {
446
logger.Infof("warning: error closing down server: %#v", err)
451
// remove removes all the files associated with csv.
452
func (csv *charmStoreVersion) remove() error {
453
return os.RemoveAll(csv.rootDir)
456
// uploadSpec specifies a entity to be uploaded through
458
type uploadSpec struct {
459
// usePost specifies that POST should be used rather than PUT.
461
// entity holds the entity to be uploaded.
463
// id holds the charm id to be uploaded to.
465
// promulgatedId holds the promulgated id to be used,
466
// valid only when usePost is false.
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 {
475
if err := csv.uploadWithPost(apiVersion, spec.entity, spec.id); err != nil {
476
return errgo.Mask(err)
479
if err := csv.uploadWithPut(apiVersion, spec.entity, spec.id, spec.promulgatedId); err != nil {
480
return errgo.Mask(err)
487
func (csv *charmStoreVersion) uploadWithPost(apiVersion string, entity ArchiverTo, url string) error {
489
if err := entity.ArchiveTo(&buf); err != nil {
490
return errgo.Mask(err)
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)
497
return errgo.Mask(err)
499
req.Header.Set("Content-Type", "application/zip")
500
resp, err := csv.DoRequest(req)
502
return errgo.Mask(err)
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)
512
func (csv *charmStoreVersion) uploadWithPut(apiVersion string, entity ArchiverTo, url, promulgatedURL string) error {
514
if err := entity.ArchiveTo(&buf); err != nil {
515
return errgo.Mask(err)
517
promulgatedParam := ""
518
if promulgatedURL != "" {
519
promulgatedParam = fmt.Sprintf("&promulgated=%s", promulgatedURL)
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)
526
return errgo.Mask(err)
528
req.Header.Set("Content-Type", "application/zip")
529
resp, err := csv.DoRequest(req)
531
return errgo.Mask(err)
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)
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)
546
return errgo.Mask(err)
548
req, err := http.NewRequest("PUT", urlPath, bytes.NewReader(data))
550
return errgo.Mask(err)
552
req.Header.Set("Content-Type", "application/json")
553
resp, err := csv.DoRequest(req)
555
return errgo.Mask(err)
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)
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)
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)
578
req, err := http.NewRequest("GET", "/", nil)
580
return errgo.Mask(err)
582
resp, err := csv.DoRequest(req)
587
if time.Now().After(endt) {
588
return errgo.Notef(err, "timed out waiting for server to come up")
590
time.Sleep(100 * time.Millisecond)
595
// startCS starts the charmd process running.
596
func (csv *charmStoreVersion) startCS(cfg *config.Config) error {
597
data, err := yaml.Marshal(cfg)
599
return errgo.Mask(err)
601
cfgPath := filepath.Join(csv.rootDir, "csconfig.yaml")
602
if err := ioutil.WriteFile(cfgPath, data, 0666); err != nil {
603
return errgo.Mask(err)
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)
613
csv.tomb.Go(func() error {
614
return errgo.Mask(cmd.Wait())
616
if err := csv.waitUntilServerIsUp(10 * time.Second); err != nil {
617
return errgo.Mask(err)
622
// Kill kills the charmstore server.
623
func (csv *charmStoreVersion) Kill() {
624
csv.runningCmd.Process.Kill()
627
// Wait waits for the charmstore server to exit.
628
func (csv *charmStoreVersion) Wait() error {
629
return csv.tomb.Wait()
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,
640
cmd.Stdout = os.Stdout
641
cmd.Stderr = os.Stderr
643
if err := cmd.Run(); err != nil {
644
return errgo.Notef(err, "failed to run %v %v", c, arg)
649
// envWithVars returns the OS environment variables
650
// with the specified variables changed to their associated
652
func envWithVars(vars map[string]string) []string {
654
for i, v := range env {
655
j := strings.Index(v, "=")
660
if val, ok := vars[name]; ok {
661
env[i] = name + "=" + val
665
for name, val := range vars {
666
env = append(env, name+"="+val)
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)
676
return errgo.Mask(err)
679
for scan := bufio.NewScanner(f); scan.Scan(); {
681
tabIndex := strings.Index(line, "\t")
683
return errgo.Newf("no tab found in dependencies line %q", line)
685
pkgPath := line[0:tabIndex]
686
if err := csv.copyRepo(pkgPath); err != nil {
687
return errgo.Mask(err)
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)
698
return errgo.Mask(err)
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)
704
if err := fs.Copy(pkg.Dir, destDir); err != nil {
705
return errgo.Mask(err)