github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 "fmt" 8 "regexp" 9 "strings" 10 11 "github.com/juju/charm/v12" 12 "github.com/juju/collections/set" 13 "github.com/juju/errors" 14 "github.com/juju/mgo/v3" 15 "github.com/juju/mgo/v3/bson" 16 "github.com/juju/mgo/v3/txn" 17 "github.com/juju/names/v5" 18 jujutxn "github.com/juju/txn/v3" 19 20 corebase "github.com/juju/juju/core/base" 21 corecharm "github.com/juju/juju/core/charm" 22 "github.com/juju/juju/mongo" 23 mongoutils "github.com/juju/juju/mongo/utils" 24 stateerrors "github.com/juju/juju/state/errors" 25 "github.com/juju/juju/state/storage" 26 jujuversion "github.com/juju/juju/version" 27 ) 28 29 // Channel identifies and describes completely a store channel. 30 type Channel struct { 31 Track string `bson:"track,omitempty"` 32 Risk string `bson:"risk"` 33 Branch string `bson:"branch,omitempty"` 34 } 35 36 // Base identifies the base os the charm was installed on. 37 type Base struct { 38 OS string `bson:"os"` 39 Channel string `bson:"channel"` 40 } 41 42 // Normalise ensures the channel always has a risk. 43 func (b Base) Normalise() Base { 44 if strings.Contains(b.Channel, "/") { 45 return b 46 } 47 nb := b 48 nb.Channel = b.Channel + "/stable" 49 return nb 50 } 51 52 func (b Base) compatibleWith(other Base) bool { 53 if b.OS != other.OS { 54 return false 55 } 56 c1, err := corebase.ParseChannel(b.Channel) 57 if err != nil { 58 return false 59 } 60 c2, err := corebase.ParseChannel(other.Channel) 61 if err != nil { 62 return false 63 } 64 return c1 == c2 65 } 66 67 // DisplayString prints the base without the rask component. 68 func (b Base) DisplayString() string { 69 if b.OS == "" || b.Channel == "" { 70 return "" 71 } 72 return fmt.Sprintf("%s@%s", b.OS, strings.Split(b.Channel, "/")[0]) 73 } 74 75 func (b Base) String() string { 76 if b.OS == "" || b.Channel == "" { 77 return "" 78 } 79 return fmt.Sprintf("%s@%s", b.OS, b.Channel) 80 } 81 82 // UbuntuBase is used in tests. 83 func UbuntuBase(channel string) Base { 84 return Base{OS: corebase.UbuntuOS, Channel: channel + "/stable"} 85 } 86 87 // DefaultLTSBase is used in tests. 88 func DefaultLTSBase() Base { 89 return Base{OS: corebase.UbuntuOS, Channel: jujuversion.DefaultSupportedLTSBase().Channel.String()} 90 } 91 92 // Platform identifies the platform the charm was installed on. 93 type Platform struct { 94 Architecture string `bson:"architecture,omitempty"` 95 OS string `bson:"os"` 96 Channel string `bson:"channel"` 97 } 98 99 // CharmOrigin holds the original source of a charm. Information about where the 100 // charm was installed from (charm-hub, charm-store, local) and any additional 101 // information we can utilise when making modelling decisions for upgrading or 102 // changing. 103 // Note: InstanceKey should never be added here. See core charm origin definition. 104 type CharmOrigin struct { 105 Source string `bson:"source"` 106 Type string `bson:"type,omitempty"` 107 ID string `bson:"id"` 108 Hash string `bson:"hash"` 109 Revision *int `bson:"revision,omitempty"` 110 Channel *Channel `bson:"channel,omitempty"` 111 Platform *Platform `bson:"platform"` 112 } 113 114 // AsCoreCharmOrigin converts a state Origin type into a core/charm.Origin. 115 func (o CharmOrigin) AsCoreCharmOrigin() corecharm.Origin { 116 origin := corecharm.Origin{ 117 Source: corecharm.Source(o.Source), 118 Type: o.Type, 119 ID: o.ID, 120 Hash: o.Hash, 121 Revision: o.Revision, 122 } 123 124 if o.Channel != nil { 125 origin.Channel = &charm.Channel{ 126 Track: o.Channel.Track, 127 Risk: charm.Risk(o.Channel.Risk), 128 Branch: o.Channel.Branch, 129 } 130 } 131 132 if o.Platform != nil { 133 origin.Platform = corecharm.Platform{ 134 Architecture: o.Platform.Architecture, 135 OS: o.Platform.OS, 136 Channel: o.Platform.Channel, 137 } 138 } 139 140 return origin 141 } 142 143 // charmDoc represents the internal state of a charm in MongoDB. 144 type charmDoc struct { 145 ModelUUID string `bson:"model-uuid"` 146 DocID string `bson:"_id"` 147 URL *string `bson:"url"` 148 // CharmVersion 149 CharmVersion string `bson:"charm-version"` 150 151 // Life manages charm lifetime in the usual way, but only local 152 // charms can actually be "destroyed"; store charms are 153 // immortal. 154 Life Life `bson:"life"` 155 156 // These fields are flags; if any of them is set, the charm 157 // cannot actually be safely used for anything. 158 PendingUpload bool `bson:"pendingupload"` 159 Placeholder bool `bson:"placeholder"` 160 161 // These fields control access to the charm archive. 162 BundleSha256 string `bson:"bundlesha256"` 163 StoragePath string `bson:"storagepath"` 164 165 // The remaining fields hold data sufficient to define a 166 // charm.Charm. 167 168 // TODO(fwereade) 2015-06-18 lp:1467964 169 // DANGEROUS: our schema can change any time the charm package changes, 170 // and we have no automated way to detect when that happens. We *must* 171 // not depend upon serializations we cannot control from inside this 172 // package. What's in a *charm.Meta? What will be tomorrow? What logic 173 // will we be writing on the assumption that all stored Metas have set 174 // some field? What fields might lose precision when they go into the 175 // database? 176 Meta *charm.Meta `bson:"meta"` 177 Config *charm.Config `bson:"config"` 178 Manifest *charm.Manifest `bson:"manifest"` 179 Actions *charm.Actions `bson:"actions"` 180 Metrics *charm.Metrics `bson:"metrics"` 181 LXDProfile *LXDProfile `bson:"lxd-profile"` 182 } 183 184 // LXDProfile is the same as ProfilePut defined in github.com/canonical/lxd/shared/api/profile.go 185 type LXDProfile struct { 186 Config map[string]string `bson:"config"` 187 Description string `bson:"description"` 188 Devices map[string]map[string]string `bson:"devices"` 189 } 190 191 // Empty returns true if neither devices nor config have been defined in the profile. 192 func (profile *LXDProfile) Empty() bool { 193 return len(profile.Devices) < 1 && len(profile.Config) < 1 194 } 195 196 // ValidateConfigDevices validates the Config and Devices properties of the LXDProfile. 197 // WhiteList devices: unix-char, unix-block, gpu, usb. 198 // BlackList config: boot*, limits* and migration*. 199 // An empty profile will not return an error. 200 // TODO (stickupkid): Remove this by moving this up into the API server layer. 201 func (profile *LXDProfile) ValidateConfigDevices() error { 202 for _, val := range profile.Devices { 203 goodDevs := set.NewStrings("unix-char", "unix-block", "gpu", "usb") 204 if devType, ok := val["type"]; ok { 205 if !goodDevs.Contains(devType) { 206 return fmt.Errorf("invalid lxd-profile.yaml: contains device type %q", devType) 207 } 208 } 209 } 210 for key := range profile.Config { 211 if strings.HasPrefix(key, "boot") || 212 strings.HasPrefix(key, "limits") || 213 strings.HasPrefix(key, "migration") { 214 return fmt.Errorf("invalid lxd-profile.yaml: contains config value %q", key) 215 } 216 } 217 return nil 218 } 219 220 // CharmInfo contains all the data necessary to store a charm's metadata. 221 type CharmInfo struct { 222 Charm charm.Charm 223 ID string 224 StoragePath string 225 SHA256 string 226 Version string 227 } 228 229 // insertCharmOps returns the txn operations necessary to insert the supplied 230 // charm data. If curl is nil, an error will be returned. 231 func insertCharmOps(mb modelBackend, info CharmInfo) ([]txn.Op, error) { 232 if info.ID == "" { 233 return nil, errors.New("charm ID was empty") 234 } 235 236 pendingUpload := info.SHA256 == "" || info.StoragePath == "" 237 238 infoIDStr := info.ID 239 doc := charmDoc{ 240 DocID: infoIDStr, 241 URL: &infoIDStr, 242 CharmVersion: info.Version, 243 Meta: info.Charm.Meta(), 244 Config: safeConfig(info.Charm), 245 Manifest: info.Charm.Manifest(), 246 Metrics: info.Charm.Metrics(), 247 Actions: info.Charm.Actions(), 248 BundleSha256: info.SHA256, 249 StoragePath: info.StoragePath, 250 PendingUpload: pendingUpload, 251 } 252 lpc, ok := info.Charm.(charm.LXDProfiler) 253 if !ok { 254 return nil, errors.New("charm does no implement LXDProfiler") 255 } 256 doc.LXDProfile = safeLXDProfile(lpc.LXDProfile()) 257 258 if err := checkCharmDataIsStorable(doc); err != nil { 259 return nil, errors.Trace(err) 260 } 261 262 return insertAnyCharmOps(mb, &doc) 263 } 264 265 // insertPlaceholderCharmOps returns the txn operations necessary to insert a 266 // charm document referencing a store charm that is not yet directly accessible 267 // within the model. If curl is empty, an error will be returned. 268 func insertPlaceholderCharmOps(mb modelBackend, curl string) ([]txn.Op, error) { 269 if curl == "" { 270 return nil, errors.BadRequestf("charm URL is empty") 271 } 272 return insertAnyCharmOps(mb, &charmDoc{ 273 DocID: curl, 274 URL: &curl, 275 Placeholder: true, 276 }) 277 } 278 279 // insertPendingCharmOps returns the txn operations necessary to insert a charm 280 // document referencing a charm that has yet to be uploaded to the model. 281 // If curl is empty, an error will be returned. 282 func insertPendingCharmOps(mb modelBackend, curl string) ([]txn.Op, error) { 283 if curl == "" { 284 return nil, errors.BadRequestf("charm URL is empty") 285 } 286 return insertAnyCharmOps(mb, &charmDoc{ 287 DocID: curl, 288 URL: &curl, 289 PendingUpload: true, 290 }) 291 } 292 293 // insertAnyCharmOps returns the txn operations necessary to insert the supplied 294 // charm document. 295 func insertAnyCharmOps(mb modelBackend, cdoc *charmDoc) ([]txn.Op, error) { 296 charms, cCloser := mb.db().GetCollection(charmsC) 297 defer cCloser() 298 299 life, err := nsLife.read(charms, cdoc.DocID) 300 if errors.IsNotFound(err) { 301 // everything is as it should be 302 } else if err != nil { 303 return nil, errors.Trace(err) 304 } else if life == Dead { 305 return nil, errors.New("url already consumed") 306 } else { 307 return nil, errors.AlreadyExistsf("charm %q", cdoc.DocID) 308 } 309 charmOp := txn.Op{ 310 C: charmsC, 311 Id: cdoc.DocID, 312 Assert: txn.DocMissing, 313 Insert: cdoc, 314 } 315 316 refcounts, rCloser := mb.db().GetCollection(refcountsC) 317 defer rCloser() 318 319 charmKey := charmGlobalKey(cdoc.URL) 320 refOp, required, err := nsRefcounts.LazyCreateOp(refcounts, charmKey) 321 if err != nil { 322 return nil, errors.Trace(err) 323 } else if required { 324 return []txn.Op{refOp, charmOp}, nil 325 } 326 return []txn.Op{charmOp}, nil 327 } 328 329 // updateCharmOps returns the txn operations necessary to update the charm 330 // document with the supplied data, so long as the supplied assert still holds 331 // true. 332 func updateCharmOps(mb modelBackend, info CharmInfo, assert bson.D) ([]txn.Op, error) { 333 charms, closer := mb.db().GetCollection(charmsC) 334 defer closer() 335 336 charmKey := info.ID 337 op, err := nsLife.aliveOp(charms, charmKey) 338 if err != nil { 339 return nil, errors.Annotate(err, "charm") 340 } 341 lifeAssert, ok := op.Assert.(bson.D) 342 if !ok { 343 return nil, errors.Errorf("expected bson.D, got %#v", op.Assert) 344 } 345 op.Assert = append(lifeAssert, assert...) 346 347 pendingUpload := info.SHA256 == "" || info.StoragePath == "" 348 349 data := bson.D{ 350 {"charm-version", info.Version}, 351 {"meta", info.Charm.Meta()}, 352 {"config", safeConfig(info.Charm)}, 353 {"actions", info.Charm.Actions()}, 354 {"manifest", info.Charm.Manifest()}, 355 {"metrics", info.Charm.Metrics()}, 356 {"storagepath", info.StoragePath}, 357 {"bundlesha256", info.SHA256}, 358 {"pendingupload", pendingUpload}, 359 {"placeholder", false}, 360 } 361 362 lpc, ok := info.Charm.(charm.LXDProfiler) 363 if !ok { 364 return nil, errors.New("charm doesn't have LXDCharmProfile()") 365 } 366 data = append(data, bson.DocElem{"lxd-profile", safeLXDProfile(lpc.LXDProfile())}) 367 368 if err := checkCharmDataIsStorable(data); err != nil { 369 return nil, errors.Trace(err) 370 } 371 372 op.Update = bson.D{{"$set", data}} 373 return []txn.Op{op}, nil 374 } 375 376 // convertPlaceholderCharmOps returns the txn operations necessary to convert 377 // the charm with the supplied docId from a placeholder to one marked for 378 // pending upload. 379 func convertPlaceholderCharmOps(docID string) ([]txn.Op, error) { 380 return []txn.Op{{ 381 C: charmsC, 382 Id: docID, 383 Assert: bson.D{ 384 {"bundlesha256", ""}, 385 {"pendingupload", false}, 386 {"placeholder", true}, 387 }, 388 Update: bson.D{{"$set", bson.D{ 389 {"pendingupload", true}, 390 {"placeholder", false}, 391 }}}, 392 }}, nil 393 394 } 395 396 // deleteOldPlaceholderCharmsOps returns the txn ops required to delete all placeholder charm 397 // records older than the specified charm URL. 398 func deleteOldPlaceholderCharmsOps(mb modelBackend, charms mongo.Collection, curl *charm.URL) ([]txn.Op, error) { 399 // Get a regex with the charm URL and no revision. 400 noRevURL := curl.WithRevision(-1) 401 curlRegex := "^" + regexp.QuoteMeta(mb.docID(noRevURL.String())) 402 403 var docs []charmDoc 404 query := bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}} 405 err := charms.Find(query).Select(bson.D{{"_id", 1}, {"url", 1}}).All(&docs) 406 if err != nil { 407 return nil, errors.Trace(err) 408 } 409 410 refcounts, closer := mb.db().GetCollection(refcountsC) 411 defer closer() 412 413 var ops []txn.Op 414 for _, doc := range docs { 415 docURL, err := charm.ParseURL(*doc.URL) 416 if err != nil { 417 return nil, errors.Trace(err) 418 } 419 if docURL.Revision >= curl.Revision { 420 continue 421 } 422 key := charmGlobalKey(doc.URL) 423 refOp, err := nsRefcounts.RemoveOp(refcounts, key, 0) 424 if err != nil { 425 return nil, errors.Trace(err) 426 } 427 ops = append(ops, refOp, txn.Op{ 428 C: charms.Name(), 429 Id: doc.DocID, 430 Assert: stillPlaceholder, 431 Remove: true, 432 }) 433 } 434 return ops, nil 435 } 436 437 func checkCharmDataIsStorable(charmData interface{}) error { 438 err := mongoutils.CheckStorable(charmData) 439 return errors.Annotate(err, "invalid charm data") 440 } 441 442 // safeConfig is a travesty which attempts to work around our continued failure 443 // to properly insulate our database from code changes; it escapes mongo- 444 // significant characters in config options. See lp:1467964. 445 func safeConfig(ch charm.Charm) *charm.Config { 446 // Make sure we escape any "$" and "." in config option names 447 // first. See http://pad.lv/1308146. 448 cfg := ch.Config() 449 escapedConfig := charm.NewConfig() 450 for optionName, option := range cfg.Options { 451 escapedName := mongoutils.EscapeKey(optionName) 452 escapedConfig.Options[escapedName] = option 453 } 454 return escapedConfig 455 } 456 457 // safeLXDProfile ensures that the LXDProfile that we put into the mongo data 458 // store, can in fact store the profile safely by escaping mongo- 459 // significant characters in config options. 460 func safeLXDProfile(profile *charm.LXDProfile) *LXDProfile { 461 if profile == nil { 462 return nil 463 } 464 escapedProfile := &LXDProfile{} 465 escapedProfile.Description = profile.Description 466 // we know the size and shape of the type, so let's use EscapeKey 467 escapedConfig := make(map[string]string, len(profile.Config)) 468 for k, v := range profile.Config { 469 escapedConfig[mongoutils.EscapeKey(k)] = v 470 } 471 escapedProfile.Config = escapedConfig 472 // this is more easy to reason about than using mongoutils.EscapeKeys, which 473 // requires looping from map[string]interface{} -> map[string]map[string]string 474 escapedDevices := make(map[string]map[string]string, len(profile.Devices)) 475 for k, v := range profile.Devices { 476 nested := make(map[string]string, len(v)) 477 for vk, vv := range v { 478 nested[mongoutils.EscapeKey(vk)] = vv 479 } 480 escapedDevices[mongoutils.EscapeKey(k)] = nested 481 } 482 escapedProfile.Devices = escapedDevices 483 return escapedProfile 484 } 485 486 // Charm represents the state of a charm in the model. 487 type Charm struct { 488 st *State 489 doc charmDoc 490 charmURL *charm.URL 491 } 492 493 func newCharm(st *State, cdoc *charmDoc) *Charm { 494 // Because we probably just read the doc from state, make sure we 495 // unescape any config option names for "$" and ".". See 496 // http://pad.lv/1308146 497 if cdoc != nil && cdoc.Config != nil { 498 unescapedConfig := charm.NewConfig() 499 for optionName, option := range cdoc.Config.Options { 500 unescapedName := mongoutils.UnescapeKey(optionName) 501 unescapedConfig.Options[unescapedName] = option 502 } 503 cdoc.Config = unescapedConfig 504 } 505 506 if cdoc != nil { 507 cdoc.LXDProfile = unescapeLXDProfile(cdoc.LXDProfile) 508 } 509 510 cdoc.ModelUUID = st.ModelUUID() 511 512 ch := Charm{st: st, doc: *cdoc} 513 return &ch 514 } 515 516 // unescapeLXDProfile returns the LXDProfile back to normal after 517 // reading from state. 518 func unescapeLXDProfile(profile *LXDProfile) *LXDProfile { 519 if profile == nil { 520 return nil 521 } 522 unescapedProfile := &LXDProfile{} 523 unescapedProfile.Description = profile.Description 524 // we know the size and shape of the type, so let's use UnescapeKey 525 unescapedConfig := make(map[string]string, len(profile.Config)) 526 for k, v := range profile.Config { 527 unescapedConfig[mongoutils.UnescapeKey(k)] = v 528 } 529 unescapedProfile.Config = unescapedConfig 530 // this is more easy to reason about than using mongoutils.UnescapeKeys, which 531 // requires looping from map[string]interface{} -> map[string]map[string]string 532 unescapedDevices := make(map[string]map[string]string, len(profile.Devices)) 533 for k, v := range profile.Devices { 534 nested := make(map[string]string, len(v)) 535 for vk, vv := range v { 536 nested[mongoutils.UnescapeKey(vk)] = vv 537 } 538 unescapedDevices[mongoutils.UnescapeKey(k)] = nested 539 } 540 unescapedProfile.Devices = unescapedDevices 541 return unescapedProfile 542 } 543 544 // Tag returns a tag identifying the charm. 545 // Implementing state.GlobalEntity interface. 546 func (c *Charm) Tag() names.Tag { 547 return names.NewCharmTag(c.URL()) 548 } 549 550 // Life returns the charm's life state. 551 func (c *Charm) Life() Life { 552 return c.doc.Life 553 } 554 555 // Refresh loads fresh charm data from the database. In practice, the 556 // only observable change should be to its Life value. 557 func (c *Charm) Refresh() error { 558 ch, err := c.st.Charm(c.URL()) 559 if err != nil { 560 return errors.Trace(err) 561 } 562 c.doc = ch.doc 563 return nil 564 } 565 566 // Destroy sets the charm to Dying and prevents it from being used by 567 // applications or units. It only works on local charms, and only when 568 // the charm is not referenced by any application. 569 func (c *Charm) Destroy() error { 570 buildTxn := func(_ int) ([]txn.Op, error) { 571 ops, err := charmDestroyOps(c.st, c.URL()) 572 if IsNotAlive(err) { 573 return nil, jujutxn.ErrNoOperations 574 } else if err != nil { 575 return nil, errors.Trace(err) 576 } 577 return ops, nil 578 } 579 if err := c.st.db().Run(buildTxn); err != nil { 580 return errors.Trace(err) 581 } 582 c.doc.Life = Dying 583 return nil 584 } 585 586 // Remove will delete the charm's stored archive and render the charm 587 // inaccessible to future clients. It will fail unless the charm is 588 // already Dying (indicating that someone has called Destroy). 589 func (c *Charm) Remove() error { 590 if c.doc.Life == Alive { 591 return errors.New("still alive") 592 } 593 594 stor := storage.NewStorage(c.st.ModelUUID(), c.st.MongoSession()) 595 err := stor.Remove(c.doc.StoragePath) 596 if errors.IsNotFound(err) { 597 // Not a problem, but we might still need to run the 598 // transaction further down to complete the process. 599 } else if err != nil { 600 return errors.Annotate(err, "deleting archive") 601 } 602 603 // We know the charm is already dying, dead or removed at this 604 // point (life can *never* go backwards) so an unasserted remove 605 // is safe. 606 removeOps := []txn.Op{{ 607 C: charmsC, 608 Id: c.doc.DocID, 609 Remove: true, 610 }} 611 if err := c.st.db().RunTransaction(removeOps); err != nil { 612 return errors.Trace(err) 613 } 614 c.doc.Life = Dead 615 return nil 616 } 617 618 // charmGlobalKey returns the global database key for the charm 619 // with the given url. 620 func charmGlobalKey(charmURL *string) string { 621 return "c#" + *charmURL 622 } 623 624 // GlobalKey returns the global database key for the charm. 625 // Implementing state.GlobalEntity interface. 626 func (c *Charm) globalKey() string { 627 return charmGlobalKey(c.doc.URL) 628 } 629 630 // URL returns a string which identifies the charm 631 // The string will parse into a charm.URL if required 632 func (c *Charm) URL() string { 633 if c.doc.URL == nil { 634 return "" 635 } 636 return *c.doc.URL 637 } 638 639 // Revision returns the monotonically increasing charm 640 // revision number. 641 // Parse the charm's URL on demand, if required. 642 func (c *Charm) Revision() int { 643 if c.charmURL == nil { 644 c.charmURL = charm.MustParseURL(c.URL()) 645 } 646 return c.charmURL.Revision 647 } 648 649 // Version returns the charm version. 650 func (c *Charm) Version() string { 651 return c.doc.CharmVersion 652 } 653 654 // Meta returns the metadata of the charm. 655 func (c *Charm) Meta() *charm.Meta { 656 return c.doc.Meta 657 } 658 659 // Config returns the configuration of the charm. 660 func (c *Charm) Config() *charm.Config { 661 return c.doc.Config 662 } 663 664 // Manifest returns information resulting from the charm 665 // build process such as the bases on which it can run. 666 func (c *Charm) Manifest() *charm.Manifest { 667 return c.doc.Manifest 668 } 669 670 // Metrics returns the metrics declared for the charm. 671 func (c *Charm) Metrics() *charm.Metrics { 672 return c.doc.Metrics 673 } 674 675 // Actions returns the actions definition of the charm. 676 func (c *Charm) Actions() *charm.Actions { 677 return c.doc.Actions 678 } 679 680 // LXDProfile returns the lxd profile definition of the charm. 681 func (c *Charm) LXDProfile() *LXDProfile { 682 return c.doc.LXDProfile 683 } 684 685 // StoragePath returns the storage path of the charm bundle. 686 func (c *Charm) StoragePath() string { 687 return c.doc.StoragePath 688 } 689 690 // BundleSha256 returns the SHA256 digest of the charm bundle bytes. 691 func (c *Charm) BundleSha256() string { 692 return c.doc.BundleSha256 693 } 694 695 // IsUploaded returns whether the charm has been uploaded to the 696 // model storage. Response is only valid when the charm is 697 // not a placeholder. 698 // TODO - hml - 29-Mar-2023 699 // Find a better answer than not pendingupload. Currently 700 // this method returns true for placeholder charm docs as well, 701 // thus the result cannot be evaluated independently. 702 func (c *Charm) IsUploaded() bool { 703 return !c.doc.PendingUpload 704 } 705 706 // IsPlaceholder returns whether the charm record is just a placeholder 707 // rather than representing a deployed charm. 708 func (c *Charm) IsPlaceholder() bool { 709 return c.doc.Placeholder 710 } 711 712 // AddCharm adds the ch charm with curl to the state. 713 // On success the newly added charm state is returned. 714 // 715 // TODO(achilleasa) Overwrite this implementation with the body of the 716 // AddCharmMetadata method once the server-side bundle expansion work is 717 // complete. 718 func (st *State) AddCharm(info CharmInfo) (stch *Charm, err error) { 719 charms, closer := st.db().GetCollection(charmsC) 720 defer closer() 721 722 if err := jujuversion.CheckJujuMinVersion(info.Charm.Meta().MinJujuVersion, jujuversion.Current); err != nil { 723 return nil, errors.Trace(err) 724 } 725 726 query := charms.FindId(info.ID).Select(bson.M{ 727 "placeholder": 1, 728 "pendingupload": 1, 729 }) 730 buildTxn := func(attempt int) ([]txn.Op, error) { 731 var doc charmDoc 732 if err := query.One(&doc); err == mgo.ErrNotFound { 733 curl, err := charm.ParseURL(info.ID) 734 if err != nil { 735 return nil, errors.Trace(err) 736 } 737 if charm.Local.Matches(curl.Schema) { 738 allocatedCurl, err := st.PrepareLocalCharmUpload(curl.String()) 739 if err != nil { 740 return nil, errors.Trace(err) 741 } 742 info.ID = allocatedCurl.String() 743 return updateCharmOps(st, info, stillPending) 744 } 745 return insertCharmOps(st, info) 746 } else if err != nil { 747 return nil, errors.Trace(err) 748 } else if doc.PendingUpload { 749 return updateCharmOps(st, info, stillPending) 750 } else if doc.Placeholder { 751 return updateCharmOps(st, info, stillPlaceholder) 752 } 753 return nil, errors.AlreadyExistsf("charm %q", info.ID) 754 } 755 if err = st.db().Run(buildTxn); err == nil { 756 return st.Charm(info.ID) 757 } 758 return nil, errors.Trace(err) 759 } 760 761 // AllCharms returns all charms in state. 762 func (st *State) AllCharms() ([]*Charm, error) { 763 charmsCollection, closer := st.db().GetCollection(charmsC) 764 defer closer() 765 var cdoc charmDoc 766 var charms []*Charm 767 iter := charmsCollection.Find(nsLife.notDead()).Iter() 768 for iter.Next(&cdoc) { 769 ch := newCharm(st, &cdoc) 770 charms = append(charms, ch) 771 } 772 return charms, errors.Trace(iter.Close()) 773 } 774 775 // Charm returns the charm with the given URL. Charms pending to be uploaded 776 // are returned for Charmhub charms. Charm placeholders are never returned. 777 func (st *State) Charm(curl string) (*Charm, error) { 778 parsedURL, err := charm.ParseURL(curl) 779 if err != nil { 780 return nil, err 781 } 782 ch, err := st.findCharm(parsedURL) 783 if err != nil { 784 return nil, err 785 } 786 if (!ch.IsUploaded() && !charm.CharmHub.Matches(parsedURL.Schema)) || ch.IsPlaceholder() { 787 return nil, errors.NotFoundf("charm %q", curl) 788 } 789 return ch, nil 790 } 791 792 // findCharm returns a charm matching the curl if it exists. This method should 793 // be used with deep understanding of charm deployment, refresh, charm revision 794 // updater. Most code should use Charm above. 795 // The primary direct use case is AddCharmMetadata. When we asynchronously download 796 // a charm, the metadata is inserted into the db so that part of deploying or 797 // refreshing a charm can happen before a charm is actually downloaded. Therefore 798 // it must be able to find placeholders and update them to allow for a download 799 // to happen as part of refresh. 800 func (st *State) findCharm(curl *charm.URL) (*Charm, error) { 801 var cdoc charmDoc 802 803 charms, closer := st.db().GetCollection(charmsC) 804 defer closer() 805 806 what := bson.D{ 807 {"_id", curl.String()}, 808 } 809 what = append(what, nsLife.notDead()...) 810 err := charms.Find(what).One(&cdoc) 811 if err == mgo.ErrNotFound { 812 return nil, errors.NotFoundf("charm %q", curl) 813 } 814 if err != nil { 815 return nil, errors.Annotatef(err, "cannot get charm %q", curl) 816 } 817 818 if cdoc.PendingUpload && !charm.CharmHub.Matches(curl.Schema) { 819 return nil, errors.NotFoundf("charm %q", curl) 820 } 821 return newCharm(st, &cdoc), nil 822 } 823 824 // Charm returns the charm with the given URL. Charms pending to be uploaded 825 // are returned for Charmhub charms. Charm placeholders are never returned. 826 func (st *State) CharmFromSha256(bundleSha256 string) (*Charm, error) { 827 var cdoc charmDoc 828 829 charms, closer := st.db().GetCollection(charmsC) 830 defer closer() 831 832 findExpr := fmt.Sprintf("^%s", bundleSha256) 833 what := bson.D{ 834 {"bundlesha256", bson.D{{"$regex", findExpr}}}, 835 {"placeholder", bson.D{{"$ne", true}}}, 836 } 837 what = append(what, nsLife.notDead()...) 838 err := charms.Find(what).One(&cdoc) 839 if err == mgo.ErrNotFound { 840 return nil, errors.NotFoundf("charm with sha256 %q", bundleSha256) 841 } 842 if err != nil { 843 return nil, errors.Annotatef(err, "cannot get charm with sha256 %q", bundleSha256) 844 } 845 846 charmurl, err := charm.ParseURL(*cdoc.URL) 847 if err != nil { 848 return nil, errors.Annotatef(err, "cannot parse url from charm %q", *cdoc.URL) 849 } 850 if cdoc.PendingUpload && !charm.CharmHub.Matches(charmurl.Schema) { 851 return nil, errors.NotFoundf("charm %q", charmurl.String()) 852 } 853 return newCharm(st, &cdoc), nil 854 } 855 856 // LatestPlaceholderCharm returns the latest charm described by the 857 // given URL but which is not yet deployed. 858 func (st *State) LatestPlaceholderCharm(curl *charm.URL) (*Charm, error) { 859 charms, closer := st.db().GetCollection(charmsC) 860 defer closer() 861 862 noRevURL := curl.WithRevision(-1) 863 curlRegex := "^" + regexp.QuoteMeta(st.docID(noRevURL.String())) 864 var docs []charmDoc 865 err := charms.Find(bson.D{{"_id", bson.D{{"$regex", curlRegex}}}, {"placeholder", true}}).All(&docs) 866 if err != nil { 867 return nil, errors.Annotatef(err, "cannot get charm %q", curl) 868 } 869 // Find the highest revision. 870 var latest charmDoc 871 var latestURL *charm.URL 872 for _, doc := range docs { 873 if latest.URL == nil { 874 latest = doc 875 latestURL, err = charm.ParseURL(*latest.URL) 876 if err != nil { 877 return nil, errors.Annotatef(err, "latest charm url") 878 } 879 } 880 docURL, err := charm.ParseURL(*doc.URL) 881 if err != nil { 882 return nil, errors.Annotatef(err, "current charm url") 883 } 884 if docURL.Revision > latestURL.Revision { 885 latest = doc 886 latestURL, err = charm.ParseURL(*latest.URL) 887 if err != nil { 888 return nil, errors.Annotatef(err, "latest charm url") 889 } 890 } 891 } 892 if latest.URL == nil { 893 return nil, errors.NotFoundf("placeholder charm %q", noRevURL) 894 } 895 return newCharm(st, &latest), nil 896 } 897 898 // PrepareLocalCharmUpload must be called before a local charm is 899 // uploaded to the provider storage in order to create a charm 900 // document in state. It returns the chosen unique charm URL reserved 901 // in state for the charm. 902 // 903 // The url's schema must be "local" and it must include a revision. 904 func (st *State) PrepareLocalCharmUpload(url string) (chosenURL *charm.URL, err error) { 905 // Perform a few sanity checks first. 906 curl, err := charm.ParseURL(url) 907 if err != nil { 908 return nil, errors.Trace(err) 909 } 910 if curl.Schema != "local" { 911 return nil, errors.Errorf("expected charm URL with local schema, got %q", curl) 912 } 913 if curl.Revision < 0 { 914 return nil, errors.Errorf("expected charm URL with revision, got %q", curl) 915 } 916 917 revisionSeq := charmRevSeqName(curl.WithRevision(-1).String()) 918 revision, err := sequenceWithMin(st, revisionSeq, curl.Revision) 919 if err != nil { 920 return nil, errors.Annotate(err, "unable to allocate charm revision") 921 } 922 allocatedURL := curl.WithRevision(revision) 923 924 ops, err := insertPendingCharmOps(st, allocatedURL.String()) 925 if err != nil { 926 return nil, errors.Trace(err) 927 } 928 929 if err := st.db().RunTransaction(ops); err != nil { 930 return nil, errors.Trace(err) 931 } 932 return allocatedURL, nil 933 } 934 935 const charmRevSeqPrefix = "charmrev-" 936 937 func charmRevSeqName(baseURL string) string { 938 return charmRevSeqPrefix + baseURL 939 } 940 941 func isCharmRevSeqName(name string) bool { 942 return strings.HasPrefix(name, charmRevSeqPrefix) 943 } 944 945 func isValidPlaceholderCharmURL(curl *charm.URL) bool { 946 return charm.CharmHub.Matches(curl.Schema) 947 } 948 949 // PrepareCharmUpload must be called before a charm store charm is uploaded to 950 // the provider storage in order to create a charm document in state. If a charm 951 // with the same URL is already in state, it will be returned as a *state.Charm 952 // (it can be still pending or already uploaded). Otherwise, a new charm 953 // document is added in state with just the given charm URL and 954 // PendingUpload=true, which is then returned as a *state.Charm. 955 // 956 // The url's schema must be charmhub ("ch") and it must 957 // include a revision that isn't a negative value. 958 // 959 // TODO(achilleas): This call will be removed once the server-side bundle 960 // deployment work lands. 961 func (st *State) PrepareCharmUpload(curl string) (*Charm, error) { 962 // Perform a few sanity checks first. 963 parsedURL, err := charm.ParseURL(curl) 964 if err != nil { 965 return nil, errors.Trace(err) 966 } 967 if !isValidPlaceholderCharmURL(parsedURL) { 968 return nil, errors.Errorf("expected charm URL with a valid schema, got %q", curl) 969 } 970 if parsedURL.Revision < 0 { 971 return nil, errors.Errorf("expected charm URL with revision, got %q", curl) 972 } 973 974 charms, closer := st.db().GetCollection(charmsC) 975 defer closer() 976 977 var ( 978 uploadedCharm charmDoc 979 ) 980 buildTxn := func(attempt int) ([]txn.Op, error) { 981 // Find an uploaded or pending charm with the given exact curl. 982 err := charms.FindId(curl).One(&uploadedCharm) 983 switch { 984 case err == mgo.ErrNotFound: 985 uploadedCharm = charmDoc{ 986 DocID: st.docID(curl), 987 URL: &curl, 988 PendingUpload: true, 989 } 990 return insertAnyCharmOps(st, &uploadedCharm) 991 case err != nil: 992 return nil, errors.Trace(err) 993 case uploadedCharm.Placeholder: 994 // Update the fields of the document we're returning. 995 uploadedCharm.PendingUpload = true 996 uploadedCharm.Placeholder = false 997 return convertPlaceholderCharmOps(uploadedCharm.DocID) 998 default: 999 // The charm exists and it's either uploaded or still 1000 // pending, but it's not a placeholder. In any case, 1001 // there's nothing to do. 1002 return nil, jujutxn.ErrNoOperations 1003 } 1004 } 1005 if err = st.db().Run(buildTxn); err == nil { 1006 return newCharm(st, &uploadedCharm), nil 1007 } 1008 return nil, errors.Trace(err) 1009 } 1010 1011 var ( 1012 stillPending = bson.D{{"pendingupload", true}} 1013 stillPlaceholder = bson.D{{"placeholder", true}} 1014 ) 1015 1016 // AddCharmPlaceholder creates a charm document in state for the given 1017 // charm URL, which must reference a charm from the given store. 1018 // The charm document is marked as a placeholder which means that if the charm 1019 // is to be deployed, it will need to first be uploaded to model storage. 1020 func (st *State) AddCharmPlaceholder(curl *charm.URL) (err error) { 1021 // Perform sanity checks first. 1022 if !isValidPlaceholderCharmURL(curl) { 1023 return errors.Errorf("expected charm URL with a valid schema, got %q", curl) 1024 } 1025 if curl.Revision < 0 { 1026 return errors.Errorf("expected charm URL with revision, got %q", curl) 1027 } 1028 charms, closer := st.db().GetCollection(charmsC) 1029 defer closer() 1030 1031 buildTxn := func(attempt int) ([]txn.Op, error) { 1032 // See if the charm already exists in state and exit early if that's the case. 1033 var doc charmDoc 1034 err := charms.Find(bson.D{{"_id", curl.String()}}).Select(bson.D{{"_id", 1}}).One(&doc) 1035 if err != nil && err != mgo.ErrNotFound { 1036 return nil, errors.Trace(err) 1037 } 1038 if err == nil { 1039 return nil, jujutxn.ErrNoOperations 1040 } 1041 1042 // Delete all previous placeholders so we don't fill up the database with unused data. 1043 deleteOps, err := deleteOldPlaceholderCharmsOps(st, charms, curl) 1044 if err != nil { 1045 return nil, errors.Trace(err) 1046 } 1047 insertOps, err := insertPlaceholderCharmOps(st, curl.String()) 1048 if err != nil { 1049 return nil, errors.Trace(err) 1050 } 1051 ops := append(deleteOps, insertOps...) 1052 return ops, nil 1053 } 1054 return errors.Trace(st.db().Run(buildTxn)) 1055 } 1056 1057 // UpdateUploadedCharm marks the given charm URL as uploaded and 1058 // updates the rest of its data, returning it as *state.Charm. 1059 // 1060 // TODO(achilleas): This call will be removed once the server-side bundle 1061 // deployment work lands. 1062 func (st *State) UpdateUploadedCharm(info CharmInfo) (*Charm, error) { 1063 charms, closer := st.db().GetCollection(charmsC) 1064 defer closer() 1065 1066 doc := &charmDoc{} 1067 err := charms.FindId(info.ID).One(&doc) 1068 if err == mgo.ErrNotFound { 1069 return nil, errors.NotFoundf("charm %q", info.ID) 1070 } 1071 if err != nil { 1072 return nil, errors.Trace(err) 1073 } 1074 if !doc.PendingUpload { 1075 return nil, errors.Trace(newErrCharmAlreadyUploaded(info.ID)) 1076 } 1077 1078 ops, err := updateCharmOps(st, info, stillPending) 1079 if err != nil { 1080 return nil, errors.Trace(err) 1081 } 1082 if err := st.db().RunTransaction(ops); err != nil { 1083 return nil, onAbort(err, stateerrors.ErrCharmRevisionAlreadyModified) 1084 } 1085 return st.Charm(info.ID) 1086 } 1087 1088 // AddCharmMetadata creates a charm document in state and populates it with the 1089 // provided charm metadata details. If the charm document already exists it 1090 // will be returned back as a *charm.Charm. 1091 // 1092 // If the charm document already exists as a placeholder and the charm hasn't 1093 // been downloaded yet, the document is updated with the current charm info. 1094 // 1095 // If the provided CharmInfo does not include a SHA256 and storage path entry, 1096 // then the charm document will be created with the PendingUpload flag set 1097 // to true. 1098 // 1099 // The charm URL must either have a charmhub ("ch") schema and it must include 1100 // a revision that isn't a negative value. Otherwise, an error will be returned. 1101 func (st *State) AddCharmMetadata(info CharmInfo) (*Charm, error) { 1102 // Perform a few sanity checks first. 1103 curl, err := charm.ParseURL(info.ID) 1104 if err != nil { 1105 return nil, errors.Trace(err) 1106 } 1107 if !isValidPlaceholderCharmURL(curl) { 1108 return nil, errors.Errorf("expected charm URL with a valid schema, got %q", info.ID) 1109 } 1110 if curl.Revision < 0 { 1111 return nil, errors.Errorf("expected charm URL with revision, got %q", info.ID) 1112 } 1113 1114 buildTxn := func(attempt int) ([]txn.Op, error) { 1115 // Check if the charm doc already exists. 1116 ch, err := st.findCharm(curl) 1117 if errors.Is(err, errors.NotFound) { 1118 return insertCharmOps(st, info) 1119 } else if err != nil { 1120 return nil, errors.Trace(err) 1121 } 1122 if !ch.IsPlaceholder() { 1123 // The charm has already been downloaded, no need to 1124 // so again. 1125 return nil, jujutxn.ErrNoOperations 1126 } 1127 // This doc was inserted by the charm revision updater worker. 1128 // Add the charm metadata and mark for download. 1129 assert := bson.D{ 1130 {"life", Alive}, 1131 {"placeholder", true}, 1132 } 1133 return updateCharmOps(st, info, assert) 1134 } 1135 1136 if err := st.db().Run(buildTxn); err != nil { 1137 return nil, errors.Trace(err) 1138 } 1139 1140 ch, err := st.Charm(info.ID) 1141 if err != nil { 1142 return nil, errors.Trace(err) 1143 } 1144 return ch, nil 1145 } 1146 1147 // AllCharmURLs returns a slice of strings representing charm.URLs for every 1148 // charm deployed in this model. 1149 func (st *State) AllCharmURLs() ([]*string, error) { 1150 applications, closer := st.db().GetCollection(charmsC) 1151 defer closer() 1152 1153 var docs []struct { 1154 CharmURL *string `bson:"url"` 1155 } 1156 err := applications.Find(bson.D{}).All(&docs) 1157 if err == mgo.ErrNotFound { 1158 return nil, errors.NotFoundf("charms") 1159 } 1160 if err != nil { 1161 return nil, errors.Errorf("cannot get all charm URLs") 1162 } 1163 1164 curls := make([]*string, len(docs)) 1165 for i, v := range docs { 1166 curls[i] = v.CharmURL 1167 } 1168 1169 return curls, nil 1170 }