github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/state/charm.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "regexp" 8 9 "github.com/juju/errors" 10 jujutxn "github.com/juju/txn" 11 "github.com/juju/version" 12 "gopkg.in/juju/charm.v6-unstable" 13 "gopkg.in/juju/names.v2" 14 "gopkg.in/macaroon.v1" 15 "gopkg.in/mgo.v2" 16 "gopkg.in/mgo.v2/bson" 17 "gopkg.in/mgo.v2/txn" 18 19 "github.com/juju/juju/mongo" 20 "github.com/juju/juju/state/storage" 21 jujuversion "github.com/juju/juju/version" 22 ) 23 24 // MacaroonCache is a type that wraps State and implements charmstore.MacaroonCache. 25 type MacaroonCache struct { 26 *State 27 } 28 29 // Set stores the macaroon on the charm. 30 func (m MacaroonCache) Set(u *charm.URL, ms macaroon.Slice) error { 31 c, err := m.Charm(u) 32 if err != nil { 33 return errors.Trace(err) 34 } 35 return c.UpdateMacaroon(ms) 36 } 37 38 // Get retrieves the macaroon for the charm (if any). 39 func (m MacaroonCache) Get(u *charm.URL) (macaroon.Slice, error) { 40 c, err := m.Charm(u) 41 if err != nil { 42 return nil, errors.Trace(err) 43 } 44 return c.Macaroon() 45 } 46 47 // charmDoc represents the internal state of a charm in MongoDB. 48 type charmDoc struct { 49 DocID string `bson:"_id"` 50 URL *charm.URL `bson:"url"` // DANGEROUS see charm.* fields below 51 52 // Life manages charm lifetime in the usual way, but only local 53 // charms can actually be "destroyed"; store charms are 54 // immortal. When a local charm is removed, its document is left 55 // in place, with Life set to Dead, to ensure we don't 56 // accidentally reuse the charm URL, which must be unique within 57 // a model. 58 // 59 // Note that this aligns with the existing contract implied by 60 // Dead: that most clients should see it as not existing at all. 61 // Nothing strictly obliges us to clean up the doc. 62 Life Life `bson:"life"` 63 64 // These fields are flags; if any of them is set, the charm 65 // cannot actually be safely used for anything. 66 PendingUpload bool `bson:"pendingupload"` 67 Placeholder bool `bson:"placeholder"` 68 69 // These fields control access to the charm archive. 70 BundleSha256 string `bson:"bundlesha256"` 71 StoragePath string `bson:"storagepath"` 72 Macaroon []byte `bson:"macaroon"` 73 74 // The remaining fields hold data sufficient to define a 75 // charm.Charm. 76 77 // TODO(fwereade) 2015-06-18 lp:1467964 78 // DANGEROUS: our schema can change any time the charm package changes, 79 // and we have no automated way to detect when that happens. We *must* 80 // not depend upon serializations we cannot control from inside this 81 // package. What's in a *charm.Meta? What will be tomorrow? What logic 82 // will we be writing on the assumption that all stored Metas have set 83 // some field? What fields might lose precision when they go into the 84 // database? 85 Meta *charm.Meta `bson:"meta"` 86 Config *charm.Config `bson:"config"` 87 Actions *charm.Actions `bson:"actions"` 88 Metrics *charm.Metrics `bson:"metrics"` 89 } 90 91 // CharmInfo contains all the data necessary to store a charm's metadata. 92 type CharmInfo struct { 93 Charm charm.Charm 94 ID *charm.URL 95 StoragePath string 96 SHA256 string 97 Macaroon macaroon.Slice 98 } 99 100 // insertCharmOps returns the txn operations necessary to insert the supplied 101 // charm data. If curl is nil, an error will be returned. 102 func insertCharmOps(st *State, info CharmInfo) ([]txn.Op, error) { 103 if info.ID == nil { 104 return nil, errors.New("*charm.URL was nil") 105 } 106 107 doc := charmDoc{ 108 DocID: info.ID.String(), 109 URL: info.ID, 110 Meta: info.Charm.Meta(), 111 Config: safeConfig(info.Charm), 112 Metrics: info.Charm.Metrics(), 113 Actions: info.Charm.Actions(), 114 BundleSha256: info.SHA256, 115 StoragePath: info.StoragePath, 116 } 117 if info.Macaroon != nil { 118 mac, err := info.Macaroon.MarshalBinary() 119 if err != nil { 120 return nil, errors.Annotate(err, "can't convert macaroon to binary for storage") 121 } 122 doc.Macaroon = mac 123 } 124 return insertAnyCharmOps(st, &doc) 125 } 126 127 // insertPlaceholderCharmOps returns the txn operations necessary to insert a 128 // charm document referencing a store charm that is not yet directly accessible 129 // within the model. If curl is nil, an error will be returned. 130 func insertPlaceholderCharmOps(st *State, curl *charm.URL) ([]txn.Op, error) { 131 if curl == nil { 132 return nil, errors.New("*charm.URL was nil") 133 } 134 return insertAnyCharmOps(st, &charmDoc{ 135 DocID: curl.String(), 136 URL: curl, 137 Placeholder: true, 138 }) 139 } 140 141 // insertPendingCharmOps returns the txn operations necessary to insert a charm 142 // document referencing a charm that has yet to be uploaded to the model. 143 // If curl is nil, an error will be returned. 144 func insertPendingCharmOps(st *State, curl *charm.URL) ([]txn.Op, error) { 145 if curl == nil { 146 return nil, errors.New("*charm.URL was nil") 147 } 148 return insertAnyCharmOps(st, &charmDoc{ 149 DocID: curl.String(), 150 URL: curl, 151 PendingUpload: true, 152 }) 153 } 154 155 // insertAnyCharmOps returns the txn operations necessary to insert the supplied 156 // charm document. 157 func insertAnyCharmOps(st modelBackend, cdoc *charmDoc) ([]txn.Op, error) { 158 159 charms, closer := st.getCollection(charmsC) 160 defer closer() 161 162 life, err := nsLife.read(charms, cdoc.DocID) 163 if errors.IsNotFound(err) { 164 // everything is as it should be 165 } else if err != nil { 166 return nil, errors.Trace(err) 167 } else if life == Dead { 168 return nil, errors.New("url already consumed") 169 } else { 170 return nil, errors.New("already exists") 171 } 172 charmOp := txn.Op{ 173 C: charmsC, 174 Id: cdoc.DocID, 175 Assert: txn.DocMissing, 176 Insert: cdoc, 177 } 178 179 refcounts, closer := st.getCollection(refcountsC) 180 defer closer() 181 182 charmKey := charmGlobalKey(cdoc.URL) 183 refOp, required, err := nsRefcounts.LazyCreateOp(refcounts, charmKey) 184 if err != nil { 185 return nil, errors.Trace(err) 186 } else if required { 187 return []txn.Op{refOp, charmOp}, nil 188 } 189 return []txn.Op{charmOp}, nil 190 } 191 192 // updateCharmOps returns the txn operations necessary to update the charm 193 // document with the supplied data, so long as the supplied assert still holds 194 // true. 195 func updateCharmOps( 196 st *State, info CharmInfo, assert bson.D, 197 ) ([]txn.Op, error) { 198 199 charms, closer := st.getCollection(charmsC) 200 defer closer() 201 202 charmKey := info.ID.String() 203 op, err := nsLife.aliveOp(charms, charmKey) 204 if err != nil { 205 return nil, errors.Annotate(err, "charm") 206 } 207 lifeAssert, ok := op.Assert.(bson.D) 208 if !ok { 209 return nil, errors.Errorf("expected bson.D, got %#v", op.Assert) 210 } 211 op.Assert = append(lifeAssert, assert...) 212 213 data := bson.D{ 214 {"meta", info.Charm.Meta()}, 215 {"config", safeConfig(info.Charm)}, 216 {"actions", info.Charm.Actions()}, 217 {"metrics", info.Charm.Metrics()}, 218 {"storagepath", info.StoragePath}, 219 {"bundlesha256", info.SHA256}, 220 {"pendingupload", false}, 221 {"placeholder", false}, 222 } 223 if len(info.Macaroon) > 0 { 224 mac, err := info.Macaroon.MarshalBinary() 225 if err != nil { 226 return nil, errors.Annotate(err, "can't convert macaroon to binary for storage") 227 } 228 data = append(data, bson.DocElem{"macaroon", mac}) 229 } 230 231 op.Update = bson.D{{"$set", data}} 232 return []txn.Op{op}, nil 233 } 234 235 // convertPlaceholderCharmOps returns the txn operations necessary to convert 236 // the charm with the supplied docId from a placeholder to one marked for 237 // pending upload. 238 func convertPlaceholderCharmOps(docID string) ([]txn.Op, error) { 239 return []txn.Op{{ 240 C: charmsC, 241 Id: docID, 242 Assert: bson.D{ 243 {"bundlesha256", ""}, 244 {"pendingupload", false}, 245 {"placeholder", true}, 246 }, 247 Update: bson.D{{"$set", bson.D{ 248 {"pendingupload", true}, 249 {"placeholder", false}, 250 }}}, 251 }}, nil 252 253 } 254 255 // deleteOldPlaceholderCharmsOps returns the txn ops required to delete all placeholder charm 256 // records older than the specified charm URL. 257 func deleteOldPlaceholderCharmsOps(st *State, charms mongo.Collection, curl *charm.URL) ([]txn.Op, error) { 258 // Get a regex with the charm URL and no revision. 259 noRevURL := curl.WithRevision(-1) 260 curlRegex := "^" + regexp.QuoteMeta(st.docID(noRevURL.String())) 261 262 var docs []charmDoc 263 query := bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}} 264 err := charms.Find(query).Select(bson.D{{"_id", 1}, {"url", 1}}).All(&docs) 265 if err != nil { 266 return nil, errors.Trace(err) 267 } 268 269 refcounts, closer := st.getCollection(refcountsC) 270 defer closer() 271 272 var ops []txn.Op 273 for _, doc := range docs { 274 if doc.URL.Revision >= curl.Revision { 275 continue 276 } 277 key := charmGlobalKey(doc.URL) 278 refOp, err := nsRefcounts.RemoveOp(refcounts, key, 0) 279 if err != nil { 280 return nil, errors.Trace(err) 281 } 282 ops = append(ops, refOp, txn.Op{ 283 C: charms.Name(), 284 Id: doc.DocID, 285 Assert: stillPlaceholder, 286 Remove: true, 287 }) 288 } 289 return ops, nil 290 } 291 292 // safeConfig is a travesty which attempts to work around our continued failure 293 // to properly insulate our database from code changes; it escapes mongo- 294 // significant characters in config options. See lp:1467964. 295 func safeConfig(ch charm.Charm) *charm.Config { 296 // Make sure we escape any "$" and "." in config option names 297 // first. See http://pad.lv/1308146. 298 cfg := ch.Config() 299 escapedConfig := charm.NewConfig() 300 for optionName, option := range cfg.Options { 301 escapedName := escapeReplacer.Replace(optionName) 302 escapedConfig.Options[escapedName] = option 303 } 304 return escapedConfig 305 } 306 307 // Charm represents the state of a charm in the model. 308 type Charm struct { 309 st *State 310 doc charmDoc 311 } 312 313 func newCharm(st *State, cdoc *charmDoc) *Charm { 314 // Because we probably just read the doc from state, make sure we 315 // unescape any config option names for "$" and ".". See 316 // http://pad.lv/1308146 317 if cdoc != nil && cdoc.Config != nil { 318 unescapedConfig := charm.NewConfig() 319 for optionName, option := range cdoc.Config.Options { 320 unescapedName := unescapeReplacer.Replace(optionName) 321 unescapedConfig.Options[unescapedName] = option 322 } 323 cdoc.Config = unescapedConfig 324 } 325 ch := Charm{st: st, doc: *cdoc} 326 return &ch 327 } 328 329 // Tag returns a tag identifying the charm. 330 // Implementing state.GlobalEntity interface. 331 func (c *Charm) Tag() names.Tag { 332 return names.NewCharmTag(c.URL().String()) 333 } 334 335 // Life returns the charm's life state. 336 func (c *Charm) Life() Life { 337 return c.doc.Life 338 } 339 340 // Refresh loads fresh charm data from the database. In practice, the 341 // only observable change should be to its Life value. 342 func (c *Charm) Refresh() error { 343 ch, err := c.st.Charm(c.doc.URL) 344 if err != nil { 345 return errors.Trace(err) 346 } 347 c.doc = ch.doc 348 return nil 349 } 350 351 // Destroy sets the charm to Dying and prevents it from being used by 352 // applications or units. It only works on local charms, and only when 353 // the charm is not referenced by any application. 354 func (c *Charm) Destroy() error { 355 buildTxn := func(_ int) ([]txn.Op, error) { 356 ops, err := charmDestroyOps(c.st, c.doc.URL) 357 switch errors.Cause(err) { 358 case nil: 359 case errNotAlive: 360 return nil, jujutxn.ErrNoOperations 361 default: 362 return nil, errors.Trace(err) 363 } 364 return ops, nil 365 } 366 if err := c.st.run(buildTxn); err != nil { 367 return errors.Trace(err) 368 } 369 c.doc.Life = Dying 370 return nil 371 } 372 373 // Remove will delete the charm's stored archive and render the charm 374 // inaccessible to future clients. It will fail unless the charm is 375 // already Dying (indicating that someone has called Destroy). 376 func (c *Charm) Remove() error { 377 switch c.doc.Life { 378 case Alive: 379 return errors.New("still alive") 380 case Dead: 381 return nil 382 } 383 384 stor := storage.NewStorage(c.st.ModelUUID(), c.st.MongoSession()) 385 err := stor.Remove(c.doc.StoragePath) 386 if errors.IsNotFound(err) { 387 // Not a problem, but we might still need to run the 388 // transaction further down to complete the process. 389 } else if err != nil { 390 return errors.Annotate(err, "deleting archive") 391 } 392 393 buildTxn := func(_ int) ([]txn.Op, error) { 394 ops, err := charmRemoveOps(c.st, c.doc.URL) 395 switch errors.Cause(err) { 396 case nil: 397 case errAlreadyDead: 398 return nil, jujutxn.ErrNoOperations 399 default: 400 return nil, errors.Trace(err) 401 } 402 return ops, nil 403 } 404 if err := c.st.run(buildTxn); err != nil { 405 return errors.Trace(err) 406 } 407 c.doc.Life = Dead 408 return nil 409 } 410 411 // charmGlobalKey returns the global database key for the charm 412 // with the given url. 413 func charmGlobalKey(charmURL *charm.URL) string { 414 return "c#" + charmURL.String() 415 } 416 417 // GlobalKey returns the global database key for the charm. 418 // Implementing state.GlobalEntity interface. 419 func (c *Charm) globalKey() string { 420 return charmGlobalKey(c.doc.URL) 421 } 422 423 func (c *Charm) String() string { 424 return c.doc.URL.String() 425 } 426 427 // URL returns the URL that identifies the charm. 428 func (c *Charm) URL() *charm.URL { 429 clone := *c.doc.URL 430 return &clone 431 } 432 433 // Revision returns the monotonically increasing charm 434 // revision number. 435 func (c *Charm) Revision() int { 436 return c.doc.URL.Revision 437 } 438 439 // Meta returns the metadata of the charm. 440 func (c *Charm) Meta() *charm.Meta { 441 return c.doc.Meta 442 } 443 444 // Config returns the configuration of the charm. 445 func (c *Charm) Config() *charm.Config { 446 return c.doc.Config 447 } 448 449 // Metrics returns the metrics declared for the charm. 450 func (c *Charm) Metrics() *charm.Metrics { 451 return c.doc.Metrics 452 } 453 454 // Actions returns the actions definition of the charm. 455 func (c *Charm) Actions() *charm.Actions { 456 return c.doc.Actions 457 } 458 459 // StoragePath returns the storage path of the charm bundle. 460 func (c *Charm) StoragePath() string { 461 return c.doc.StoragePath 462 } 463 464 // BundleSha256 returns the SHA256 digest of the charm bundle bytes. 465 func (c *Charm) BundleSha256() string { 466 return c.doc.BundleSha256 467 } 468 469 // IsUploaded returns whether the charm has been uploaded to the 470 // model storage. 471 func (c *Charm) IsUploaded() bool { 472 return !c.doc.PendingUpload 473 } 474 475 // IsPlaceholder returns whether the charm record is just a placeholder 476 // rather than representing a deployed charm. 477 func (c *Charm) IsPlaceholder() bool { 478 return c.doc.Placeholder 479 } 480 481 // Macaroon return the macaroon that can be used to request data about the charm 482 // from the charmstore, or nil if the charm is not private. 483 func (c *Charm) Macaroon() (macaroon.Slice, error) { 484 if len(c.doc.Macaroon) == 0 { 485 return nil, nil 486 } 487 var m macaroon.Slice 488 if err := m.UnmarshalBinary(c.doc.Macaroon); err != nil { 489 return nil, errors.Trace(err) 490 } 491 492 return m, nil 493 } 494 495 // UpdateMacaroon updates the stored macaroon for this charm. 496 func (c *Charm) UpdateMacaroon(m macaroon.Slice) error { 497 info := CharmInfo{ 498 Charm: c, 499 ID: c.URL(), 500 StoragePath: c.StoragePath(), 501 SHA256: c.BundleSha256(), 502 Macaroon: m, 503 } 504 ops, err := updateCharmOps(c.st, info, nil) 505 if err != nil { 506 return errors.Trace(err) 507 } 508 if err := c.st.runTransaction(ops); err != nil { 509 return errors.Trace(err) 510 } 511 return nil 512 } 513 514 // AddCharm adds the ch charm with curl to the state. 515 // On success the newly added charm state is returned. 516 func (st *State) AddCharm(info CharmInfo) (stch *Charm, err error) { 517 charms, closer := st.getCollection(charmsC) 518 defer closer() 519 520 if err := validateCharmVersion(info.Charm); err != nil { 521 return nil, errors.Trace(err) 522 } 523 524 query := charms.FindId(info.ID.String()).Select(bson.D{{"placeholder", 1}}) 525 526 buildTxn := func(attempt int) ([]txn.Op, error) { 527 var placeholderDoc struct { 528 Placeholder bool `bson:"placeholder"` 529 } 530 if err := query.One(&placeholderDoc); err == mgo.ErrNotFound { 531 532 return insertCharmOps(st, info) 533 } else if err != nil { 534 return nil, errors.Trace(err) 535 } else if placeholderDoc.Placeholder { 536 return updateCharmOps(st, info, stillPlaceholder) 537 } 538 return nil, errors.AlreadyExistsf("charm %q", info.ID) 539 } 540 if err = st.run(buildTxn); err == nil { 541 return st.Charm(info.ID) 542 } 543 return nil, errors.Trace(err) 544 } 545 546 type hasMeta interface { 547 Meta() *charm.Meta 548 } 549 550 func validateCharmVersion(ch hasMeta) error { 551 minver := ch.Meta().MinJujuVersion 552 if minver != version.Zero { 553 if minver.Compare(jujuversion.Current) > 0 { 554 return errors.Errorf("Charm's min version (%s) is higher than this juju environment's version (%s)", minver, jujuversion.Current) 555 } 556 } 557 return nil 558 } 559 560 // AllCharms returns all charms in state. 561 func (st *State) AllCharms() ([]*Charm, error) { 562 charmsCollection, closer := st.getCollection(charmsC) 563 defer closer() 564 var cdoc charmDoc 565 var charms []*Charm 566 iter := charmsCollection.Find(nsLife.notDead()).Iter() 567 for iter.Next(&cdoc) { 568 ch := newCharm(st, &cdoc) 569 charms = append(charms, ch) 570 } 571 return charms, errors.Trace(iter.Close()) 572 } 573 574 // Charm returns the charm with the given URL. Charms pending upload 575 // to storage and placeholders are never returned. 576 func (st *State) Charm(curl *charm.URL) (*Charm, error) { 577 charms, closer := st.getCollection(charmsC) 578 defer closer() 579 580 cdoc := &charmDoc{} 581 what := bson.D{ 582 {"_id", curl.String()}, 583 {"placeholder", bson.D{{"$ne", true}}}, 584 {"pendingupload", bson.D{{"$ne", true}}}, 585 } 586 what = append(what, nsLife.notDead()...) 587 err := charms.Find(what).One(&cdoc) 588 if err == mgo.ErrNotFound { 589 return nil, errors.NotFoundf("charm %q", curl) 590 } 591 if err != nil { 592 return nil, errors.Annotatef(err, "cannot get charm %q", curl) 593 } 594 if err := cdoc.Meta.Check(); err != nil { 595 return nil, errors.Annotatef(err, "malformed charm metadata found in state") 596 } 597 return newCharm(st, cdoc), nil 598 } 599 600 // LatestPlaceholderCharm returns the latest charm described by the 601 // given URL but which is not yet deployed. 602 func (st *State) LatestPlaceholderCharm(curl *charm.URL) (*Charm, error) { 603 charms, closer := st.getCollection(charmsC) 604 defer closer() 605 606 noRevURL := curl.WithRevision(-1) 607 curlRegex := "^" + regexp.QuoteMeta(st.docID(noRevURL.String())) 608 var docs []charmDoc 609 err := charms.Find(bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}}).All(&docs) 610 if err != nil { 611 return nil, errors.Annotatef(err, "cannot get charm %q", curl) 612 } 613 // Find the highest revision. 614 var latest charmDoc 615 for _, doc := range docs { 616 if latest.URL == nil || doc.URL.Revision > latest.URL.Revision { 617 latest = doc 618 } 619 } 620 if latest.URL == nil { 621 return nil, errors.NotFoundf("placeholder charm %q", noRevURL) 622 } 623 return newCharm(st, &latest), nil 624 } 625 626 // PrepareLocalCharmUpload must be called before a local charm is 627 // uploaded to the provider storage in order to create a charm 628 // document in state. It returns the chosen unique charm URL reserved 629 // in state for the charm. 630 // 631 // The url's schema must be "local" and it must include a revision. 632 func (st *State) PrepareLocalCharmUpload(curl *charm.URL) (chosenURL *charm.URL, err error) { 633 // Perform a few sanity checks first. 634 if curl.Schema != "local" { 635 return nil, errors.Errorf("expected charm URL with local schema, got %q", curl) 636 } 637 if curl.Revision < 0 { 638 return nil, errors.Errorf("expected charm URL with revision, got %q", curl) 639 } 640 // Get a regex with the charm URL and no revision. 641 noRevURL := curl.WithRevision(-1) 642 curlRegex := "^" + regexp.QuoteMeta(st.docID(noRevURL.String())) 643 644 charms, closer := st.getCollection(charmsC) 645 defer closer() 646 647 buildTxn := func(attempt int) ([]txn.Op, error) { 648 // Find the highest revision of that charm in state. 649 var docs []charmDoc 650 query := bson.D{{"_id", bson.D{{"$regex", curlRegex}}}} 651 err = charms.Find(query).Select(bson.D{{"_id", 1}, {"url", 1}}).All(&docs) 652 if err != nil { 653 return nil, errors.Trace(err) 654 } 655 // Find the highest revision. 656 maxRevision := -1 657 for _, doc := range docs { 658 if doc.URL.Revision > maxRevision { 659 maxRevision = doc.URL.Revision 660 } 661 } 662 663 // Respect the local charm's revision first. 664 chosenRevision := curl.Revision 665 if maxRevision >= chosenRevision { 666 // More recent revision exists in state, pick the next. 667 chosenRevision = maxRevision + 1 668 } 669 chosenURL = curl.WithRevision(chosenRevision) 670 return insertPendingCharmOps(st, chosenURL) 671 } 672 if err = st.run(buildTxn); err == nil { 673 return chosenURL, nil 674 } 675 return nil, errors.Trace(err) 676 } 677 678 // PrepareStoreCharmUpload must be called before a charm store charm 679 // is uploaded to the provider storage in order to create a charm 680 // document in state. If a charm with the same URL is already in 681 // state, it will be returned as a *state.Charm (it can be still 682 // pending or already uploaded). Otherwise, a new charm document is 683 // added in state with just the given charm URL and 684 // PendingUpload=true, which is then returned as a *state.Charm. 685 // 686 // The url's schema must be "cs" and it must include a revision. 687 func (st *State) PrepareStoreCharmUpload(curl *charm.URL) (*Charm, error) { 688 // Perform a few sanity checks first. 689 if curl.Schema != "cs" { 690 return nil, errors.Errorf("expected charm URL with cs schema, got %q", curl) 691 } 692 if curl.Revision < 0 { 693 return nil, errors.Errorf("expected charm URL with revision, got %q", curl) 694 } 695 696 charms, closer := st.getCollection(charmsC) 697 defer closer() 698 699 var ( 700 uploadedCharm charmDoc 701 err error 702 ) 703 buildTxn := func(attempt int) ([]txn.Op, error) { 704 // Find an uploaded or pending charm with the given exact curl. 705 err := charms.FindId(curl.String()).One(&uploadedCharm) 706 switch { 707 case err == mgo.ErrNotFound: 708 uploadedCharm = charmDoc{ 709 DocID: st.docID(curl.String()), 710 URL: curl, 711 PendingUpload: true, 712 } 713 return insertAnyCharmOps(st, &uploadedCharm) 714 case err != nil: 715 return nil, errors.Trace(err) 716 case uploadedCharm.Placeholder: 717 // Update the fields of the document we're returning. 718 uploadedCharm.PendingUpload = true 719 uploadedCharm.Placeholder = false 720 return convertPlaceholderCharmOps(uploadedCharm.DocID) 721 default: 722 // The charm exists and it's either uploaded or still 723 // pending, but it's not a placeholder. In any case, 724 // there's nothing to do. 725 return nil, jujutxn.ErrNoOperations 726 } 727 } 728 if err = st.run(buildTxn); err == nil { 729 return newCharm(st, &uploadedCharm), nil 730 } 731 return nil, errors.Trace(err) 732 } 733 734 var ( 735 stillPending = bson.D{{"pendingupload", true}} 736 stillPlaceholder = bson.D{{"placeholder", true}} 737 ) 738 739 // AddStoreCharmPlaceholder creates a charm document in state for the given charm URL which 740 // must reference a charm from the store. The charm document is marked as a placeholder which 741 // means that if the charm is to be deployed, it will need to first be uploaded to env storage. 742 func (st *State) AddStoreCharmPlaceholder(curl *charm.URL) (err error) { 743 // Perform sanity checks first. 744 if curl.Schema != "cs" { 745 return errors.Errorf("expected charm URL with cs schema, got %q", curl) 746 } 747 if curl.Revision < 0 { 748 return errors.Errorf("expected charm URL with revision, got %q", curl) 749 } 750 charms, closer := st.getCollection(charmsC) 751 defer closer() 752 753 buildTxn := func(attempt int) ([]txn.Op, error) { 754 // See if the charm already exists in state and exit early if that's the case. 755 var doc charmDoc 756 err := charms.Find(bson.D{{"_id", curl.String()}}).Select(bson.D{{"_id", 1}}).One(&doc) 757 if err != nil && err != mgo.ErrNotFound { 758 return nil, errors.Trace(err) 759 } 760 if err == nil { 761 return nil, jujutxn.ErrNoOperations 762 } 763 764 // Delete all previous placeholders so we don't fill up the database with unused data. 765 deleteOps, err := deleteOldPlaceholderCharmsOps(st, charms, curl) 766 if err != nil { 767 return nil, errors.Trace(err) 768 } 769 insertOps, err := insertPlaceholderCharmOps(st, curl) 770 if err != nil { 771 return nil, errors.Trace(err) 772 } 773 ops := append(deleteOps, insertOps...) 774 return ops, nil 775 } 776 return errors.Trace(st.run(buildTxn)) 777 } 778 779 // UpdateUploadedCharm marks the given charm URL as uploaded and 780 // updates the rest of its data, returning it as *state.Charm. 781 func (st *State) UpdateUploadedCharm(info CharmInfo) (*Charm, error) { 782 charms, closer := st.getCollection(charmsC) 783 defer closer() 784 785 doc := &charmDoc{} 786 err := charms.FindId(info.ID.String()).One(&doc) 787 if err == mgo.ErrNotFound { 788 return nil, errors.NotFoundf("charm %q", info.ID) 789 } 790 if err != nil { 791 return nil, errors.Trace(err) 792 } 793 if !doc.PendingUpload { 794 return nil, errors.Trace(&ErrCharmAlreadyUploaded{info.ID}) 795 } 796 797 ops, err := updateCharmOps(st, info, stillPending) 798 if err != nil { 799 return nil, errors.Trace(err) 800 } 801 if err := st.runTransaction(ops); err != nil { 802 return nil, onAbort(err, ErrCharmRevisionAlreadyModified) 803 } 804 return st.Charm(info.ID) 805 }