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  }