github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/applicationoffers.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/juju/charm/v12"
    13  	"github.com/juju/collections/set"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/mgo/v3"
    16  	"github.com/juju/mgo/v3/bson"
    17  	"github.com/juju/mgo/v3/txn"
    18  	"github.com/juju/names/v5"
    19  	jujutxn "github.com/juju/txn/v3"
    20  	"github.com/juju/utils/v3"
    21  
    22  	"github.com/juju/juju/core/crossmodel"
    23  	"github.com/juju/juju/core/permission"
    24  )
    25  
    26  const (
    27  	// applicationOfferGlobalKey is the key for an application offer.
    28  	applicationOfferGlobalKey = "ao"
    29  )
    30  
    31  // applicationOfferKey will return the key for a given offer using the
    32  // offer uuid and the applicationOfferGlobalKey.
    33  func applicationOfferKey(offerUUID string) string {
    34  	return fmt.Sprintf("%s#%s", applicationOfferGlobalKey, offerUUID)
    35  }
    36  
    37  // applicationOfferDoc represents the internal state of a application offer in MongoDB.
    38  type applicationOfferDoc struct {
    39  	DocID string `bson:"_id"`
    40  
    41  	// OfferUUID is the UUID of the offer.
    42  	OfferUUID string `bson:"offer-uuid"`
    43  
    44  	// OfferName is the name of the offer.
    45  	OfferName string `bson:"offer-name"`
    46  
    47  	// ApplicationName is the name of the application to which an offer pertains.
    48  	ApplicationName string `bson:"application-name"`
    49  
    50  	// ApplicationDescription is a description of the application's functionality,
    51  	// typically copied from the charm metadata.
    52  	ApplicationDescription string `bson:"application-description"`
    53  
    54  	// Endpoints are the charm endpoints supported by the application.
    55  	Endpoints map[string]string `bson:"endpoints"`
    56  
    57  	// TxnRevno is used to assert the collection have not changed since this
    58  	// document was fetched.
    59  	TxnRevno int64 `bson:"txn-revno,omitempty"`
    60  }
    61  
    62  var _ crossmodel.ApplicationOffers = (*applicationOffers)(nil)
    63  
    64  type applicationOffers struct {
    65  	st *State
    66  }
    67  
    68  // NewApplicationOffers creates a application directory backed by a state instance.
    69  func NewApplicationOffers(st *State) crossmodel.ApplicationOffers {
    70  	return &applicationOffers{st: st}
    71  }
    72  
    73  // ApplicationOfferEndpoint returns from the specified offer, the relation endpoint
    74  // with the supplied name, if it exists.
    75  func ApplicationOfferEndpoint(offer crossmodel.ApplicationOffer, relationName string) (Endpoint, error) {
    76  	for _, ep := range offer.Endpoints {
    77  		if ep.Name == relationName {
    78  			return Endpoint{
    79  				ApplicationName: offer.ApplicationName,
    80  				Relation:        ep,
    81  			}, nil
    82  		}
    83  	}
    84  	return Endpoint{}, errors.NotFoundf("relation %q on application offer %q", relationName, offer.String())
    85  }
    86  
    87  func (s *applicationOffers) offerQuery(query bson.D) (*applicationOfferDoc, error) {
    88  	applicationOffersCollection, closer := s.st.db().GetCollection(applicationOffersC)
    89  	defer closer()
    90  
    91  	var doc applicationOfferDoc
    92  	err := applicationOffersCollection.Find(query).One(&doc)
    93  	return &doc, err
    94  }
    95  
    96  // ApplicationOffer returns the named application offer.
    97  func (s *applicationOffers) ApplicationOffer(offerName string) (*crossmodel.ApplicationOffer, error) {
    98  	offerDoc, err := s.applicationOfferDoc(offerName)
    99  	if err != nil {
   100  		return nil, errors.Trace(err)
   101  	}
   102  	return s.makeApplicationOffer(*offerDoc)
   103  }
   104  
   105  func (s *applicationOffers) applicationOfferDoc(offerName string) (*applicationOfferDoc, error) {
   106  	offerDoc, err := s.offerQuery(bson.D{{"_id", offerName}})
   107  	if err != nil {
   108  		if err == mgo.ErrNotFound {
   109  			return nil, errors.NotFoundf("offer %q", offerName)
   110  		}
   111  		return nil, errors.Annotatef(err, "cannot load application offer %q", offerName)
   112  	}
   113  	return offerDoc, nil
   114  }
   115  
   116  // ApplicationOfferForUUID returns the application offer for the UUID.
   117  func (s *applicationOffers) ApplicationOfferForUUID(offerUUID string) (*crossmodel.ApplicationOffer, error) {
   118  	offerDoc, err := s.offerQuery(bson.D{{"offer-uuid", offerUUID}})
   119  	if err != nil {
   120  		if err == mgo.ErrNotFound {
   121  			return nil, errors.NotFoundf("offer %q", offerUUID)
   122  		}
   123  		return nil, errors.Annotatef(err, "cannot load application offer %q", offerUUID)
   124  	}
   125  	return s.makeApplicationOffer(*offerDoc)
   126  }
   127  
   128  // AllApplicationOffers returns all application offers in the model.
   129  func (s *applicationOffers) AllApplicationOffers() (offers []*crossmodel.ApplicationOffer, _ error) {
   130  	applicationOffersCollection, closer := s.st.db().GetCollection(applicationOffersC)
   131  	defer closer()
   132  
   133  	var docs []applicationOfferDoc
   134  	err := applicationOffersCollection.Find(bson.D{}).All(&docs)
   135  	if err != nil {
   136  		return nil, errors.Annotate(err, "getting application offer documents")
   137  	}
   138  	for _, doc := range docs {
   139  		offer, err := s.makeApplicationOffer(doc)
   140  		if err != nil {
   141  			return nil, errors.Trace(err)
   142  		}
   143  		offers = append(offers, offer)
   144  	}
   145  	return offers, nil
   146  }
   147  
   148  // maybeConsumerProxyForOffer returns the remote app consumer proxy related to
   149  // the offer on the specified relation if one exists.
   150  func (st *State) maybeConsumerProxyForOffer(offer *crossmodel.ApplicationOffer, rel *Relation) (*RemoteApplication, bool, error) {
   151  	// Is the relation for an offer connection
   152  	offConn, err := st.OfferConnectionForRelation(rel.String())
   153  	if err != nil && !errors.IsNotFound(err) {
   154  		return nil, false, errors.Trace(err)
   155  	}
   156  	if err != nil || offConn.OfferUUID() != offer.OfferUUID {
   157  		return nil, false, nil
   158  	}
   159  
   160  	// Get the remote app proxy for the connection.
   161  	remoteApp, isCrossModel, err := rel.RemoteApplication()
   162  	if err != nil {
   163  		return nil, false, errors.Trace(err)
   164  	}
   165  	// Sanity check - we expect a cross model relation at this stage.
   166  	if !isCrossModel {
   167  		return nil, false, nil
   168  	}
   169  
   170  	// We have a remote app proxy, is it related to the offer in question.
   171  	_, err = rel.Endpoint(offer.ApplicationName)
   172  	if err != nil {
   173  		if !errors.IsNotFound(err) {
   174  			return nil, false, errors.Trace(err)
   175  		}
   176  		return nil, false, nil
   177  	}
   178  	return remoteApp, true, nil
   179  }
   180  
   181  // RemoveOfferOperation returns a model operation that will allow relation to leave scope.
   182  func (s *applicationOffers) RemoveOfferOperation(offerName string, force bool) (*RemoveOfferOperation, error) {
   183  	offerStore := &applicationOffers{s.st}
   184  
   185  	// Any proxies for applications on the consuming side also need to be removed.
   186  	offer, err := offerStore.ApplicationOffer(offerName)
   187  	if err != nil {
   188  		return nil, errors.Trace(err)
   189  	}
   190  	var associatedAppProxies []*DestroyRemoteApplicationOperation
   191  	if err == nil {
   192  		// Look at relations to the offer and if it is a cross model relation,
   193  		// record the associated remote app proxy in the remove operation.
   194  		rels, err := s.st.AllRelations()
   195  		if err != nil {
   196  			return nil, errors.Trace(err)
   197  		}
   198  		for _, rel := range rels {
   199  			remoteApp, isCrossModel, err := s.st.maybeConsumerProxyForOffer(offer, rel)
   200  			if err != nil {
   201  				return nil, errors.Trace(err)
   202  			}
   203  			if !isCrossModel {
   204  				continue
   205  			}
   206  			logger.Debugf("destroy consumer proxy %v for offer %v", remoteApp.Name(), offerName)
   207  			associatedAppProxies = append(associatedAppProxies, remoteApp.DestroyOperation(force))
   208  		}
   209  	}
   210  	return &RemoveOfferOperation{
   211  		offerStore:           offerStore,
   212  		offerName:            offerName,
   213  		associatedAppProxies: associatedAppProxies,
   214  		ForcedOperation:      ForcedOperation{Force: force},
   215  	}, nil
   216  }
   217  
   218  // RemoveOfferOperation is a model operation to remove application offer.
   219  type RemoveOfferOperation struct {
   220  	offerStore *applicationOffers
   221  
   222  	// ForcedOperation stores needed information to force this operation.
   223  	ForcedOperation
   224  	// offerName is the offer name to remove.
   225  	offerName string
   226  	// offer is the offer itself, set as the operation runs.
   227  	offer *crossmodel.ApplicationOffer
   228  
   229  	// associatedAppProxies are consuming model references that need
   230  	// to be removed along with the offer.
   231  	associatedAppProxies []*DestroyRemoteApplicationOperation
   232  }
   233  
   234  // Build is part of the ModelOperation interface.
   235  func (op *RemoveOfferOperation) Build(attempt int) (ops []txn.Op, err error) {
   236  	op.offer, err = op.offerStore.ApplicationOffer(op.offerName)
   237  	if errors.IsNotFound(err) {
   238  		return nil, jujutxn.ErrNoOperations
   239  	}
   240  	if err != nil {
   241  		return nil, errors.Trace(err)
   242  	}
   243  	// When 'force' is set on the operation, this call will return needed operations
   244  	// and accumulate all operational errors encountered in the operation.
   245  	// If the 'force' is not set, any error will be fatal and no operations will be returned.
   246  	switch ops, err = op.internalRemove(op.offer); err {
   247  	case errRefresh:
   248  	case errAlreadyDying:
   249  		return nil, jujutxn.ErrNoOperations
   250  	}
   251  	if err != nil {
   252  		if op.Force {
   253  			logger.Warningf("force removing offer %v despite error %v", op.offerName, err)
   254  		} else {
   255  			return nil, err
   256  		}
   257  	}
   258  	// If the offer is being removed, then any proxies for applications on the
   259  	// consuming side also need to be removed.
   260  	for _, remoteProxyOp := range op.associatedAppProxies {
   261  		proxyOps, err := remoteProxyOp.Build(attempt)
   262  		if err == jujutxn.ErrNoOperations {
   263  			continue
   264  		}
   265  		if err != nil {
   266  			if remoteProxyOp.Force {
   267  				logger.Warningf("force removing consuming proxy %v despite error %v", remoteProxyOp.app.Name(), err)
   268  			} else {
   269  				return nil, err
   270  			}
   271  		}
   272  		ops = append(ops, proxyOps...)
   273  	}
   274  	return ops, nil
   275  }
   276  
   277  // Done is part of the ModelOperation interface.
   278  func (op *RemoveOfferOperation) Done(err error) error {
   279  	if err != nil {
   280  		if !op.Force {
   281  			if errors.Cause(err) == jujutxn.ErrExcessiveContention {
   282  				relCount, err := op.countOfferRelations(op.offer)
   283  				if err != nil {
   284  					return errors.Annotatef(err, "cannot delete application offer %q", op.offerName)
   285  				}
   286  				if relCount > 0 {
   287  					return errors.Errorf("cannot delete application offer %q since its underlying application still has %d relations", op.offerName, relCount)
   288  				}
   289  			}
   290  			return errors.Annotatef(err, "cannot delete application offer %q", op.offerName)
   291  		}
   292  		op.AddError(errors.Errorf("forced offer %v removal but proceeded despite encountering ERROR %v", op.offerName, err))
   293  	}
   294  	for _, remoteProxyOp := range op.associatedAppProxies {
   295  		// Final cleanup of consuming app proxy is best effort.
   296  		if err := remoteProxyOp.Done(nil); err != nil {
   297  			op.AddError(errors.Errorf("error finalising removal of consuming proxy %q: %v", remoteProxyOp.app.Name(), err))
   298  		}
   299  	}
   300  	// Now the offer is removed, delete any user permissions.
   301  	userPerms, err := op.offerStore.st.GetOfferUsers(op.offer.OfferUUID)
   302  	if err != nil {
   303  		op.AddError(errors.Errorf("error removing offer permissions: %v", err))
   304  		return nil
   305  	}
   306  	var removeOps []txn.Op
   307  	for userName := range userPerms {
   308  		user := names.NewUserTag(userName)
   309  		removeOps = append(removeOps,
   310  			removePermissionOp(applicationOfferKey(op.offer.OfferUUID), userGlobalKey(userAccessID(user))))
   311  	}
   312  	err = op.offerStore.st.db().RunTransaction(removeOps)
   313  	if err != nil {
   314  		op.AddError(errors.Errorf("error removing offer permissions: %v", err))
   315  	}
   316  	return nil
   317  }
   318  
   319  // Remove deletes the application offer for offerName immediately.
   320  func (s *applicationOffers) Remove(offerName string, force bool) error {
   321  	op, err := s.RemoveOfferOperation(offerName, force)
   322  	if err != nil {
   323  		return errors.Trace(err)
   324  	}
   325  	err = s.st.ApplyOperation(op)
   326  	if len(op.Errors) != 0 {
   327  		logger.Warningf("operational errors removing offer %v: %v", offerName, op.Errors)
   328  	}
   329  	return err
   330  }
   331  
   332  func (op *RemoveOfferOperation) countOfferRelations(offer *crossmodel.ApplicationOffer) (int, error) {
   333  	if offer == nil {
   334  		return 0, nil
   335  	}
   336  	app, err := op.offerStore.st.Application(offer.ApplicationName)
   337  	if err != nil {
   338  		return 0, errors.Trace(err)
   339  	}
   340  	rels, err := app.Relations()
   341  	if err != nil {
   342  		return 0, errors.Trace(err)
   343  	}
   344  	var count int
   345  	for _, rel := range rels {
   346  		remoteApp, isCrossModel, err := op.offerStore.st.maybeConsumerProxyForOffer(offer, rel)
   347  		if err != nil {
   348  			return 0, errors.Trace(err)
   349  		}
   350  		if !isCrossModel || remoteApp == nil {
   351  			continue
   352  		}
   353  		count++
   354  	}
   355  	return count, nil
   356  }
   357  
   358  func (op *RemoveOfferOperation) internalRemove(offer *crossmodel.ApplicationOffer) ([]txn.Op, error) {
   359  	// Load the application before counting the connections
   360  	// so we can do a consistency check on relation count.
   361  	app, err := op.offerStore.st.Application(offer.ApplicationName)
   362  	if err != nil {
   363  		return nil, errors.Trace(err)
   364  	}
   365  	conns, err := op.offerStore.st.OfferConnections(offer.OfferUUID)
   366  	if err != nil {
   367  		return nil, errors.Trace(err)
   368  	}
   369  	if len(conns) > 0 && !op.Force {
   370  		return nil, errors.Errorf("offer has %d relation%s", len(conns), plural(len(conns)))
   371  	}
   372  	// Because we don't refcount offer connections, we instead either
   373  	// assert here that the relation count doesn't change, and that the
   374  	// specific relations that make up that count aren't removed, or we
   375  	// remove the relations, depending on whether force=true.
   376  	rels, err := app.Relations()
   377  	if err != nil {
   378  		return nil, errors.Trace(err)
   379  	}
   380  	if len(rels) != app.doc.RelationCount {
   381  		return nil, jujutxn.ErrTransientFailure
   382  	}
   383  	ops := []txn.Op{{
   384  		C:      applicationsC,
   385  		Id:     offer.ApplicationName,
   386  		Assert: bson.D{{"relationcount", app.doc.RelationCount}},
   387  	}}
   388  	for _, rel := range rels {
   389  		remoteApp, isCrossModel, err := op.offerStore.st.maybeConsumerProxyForOffer(offer, rel)
   390  		if err != nil {
   391  			return nil, errors.Trace(err)
   392  		}
   393  		if isCrossModel && !op.Force {
   394  			logger.Debugf("aborting removal of offer %q due to relation %q", offer.OfferName, rel)
   395  			return nil, jujutxn.ErrTransientFailure
   396  		}
   397  		if op.Force {
   398  			// We only force delete cross model relations (connections).
   399  			if !isCrossModel {
   400  				continue
   401  			}
   402  			if err := rel.Refresh(); errors.IsNotFound(err) {
   403  				continue
   404  			} else if err != nil {
   405  				return nil, err
   406  			}
   407  
   408  			// Force any remote units to leave scope so the offer can be cleaned up.
   409  			destroyRelUnitOps, err := destroyCrossModelRelationUnitsOps(&op.ForcedOperation, remoteApp, rel, false)
   410  			if err != nil && err != jujutxn.ErrNoOperations {
   411  				return nil, errors.Trace(err)
   412  			}
   413  			ops = append(ops, destroyRelUnitOps...)
   414  
   415  			// When 'force' is set, this call will return needed operations
   416  			// and accumulate all operational errors encountered in the operation.
   417  			// If the 'force' is not set, any error will be fatal and no operations will be returned.
   418  			relOps, _, err := rel.destroyOps("", &op.ForcedOperation)
   419  			if err == errAlreadyDying {
   420  				continue
   421  			} else if err != nil {
   422  				return nil, errors.Trace(err)
   423  			}
   424  			ops = append(ops, relOps...)
   425  		} else {
   426  			ops = append(ops, txn.Op{
   427  				C:      relationsC,
   428  				Id:     rel.doc.DocID,
   429  				Assert: txn.DocExists,
   430  			})
   431  		}
   432  	}
   433  	decRefOp, err := decApplicationOffersRefOp(op.offerStore.st, offer.ApplicationName)
   434  	if err != nil {
   435  		return nil, errors.Trace(err)
   436  	}
   437  	ops = append(ops, txn.Op{
   438  		C:      applicationOffersC,
   439  		Id:     offer.OfferName,
   440  		Assert: txn.DocExists,
   441  		Remove: true,
   442  	}, decRefOp)
   443  	r := op.offerStore.st.RemoteEntities()
   444  	tokenOps := r.removeRemoteEntityOps(names.NewApplicationTag(offer.OfferName))
   445  	ops = append(ops, tokenOps...)
   446  	return ops, nil
   447  }
   448  
   449  // applicationOffersDocs returns the offer docs for the given application
   450  func applicationOffersDocs(st *State, application string) ([]applicationOfferDoc, error) {
   451  	applicationOffersCollection, closer := st.db().GetCollection(applicationOffersC)
   452  	defer closer()
   453  	query := bson.D{{"application-name", application}}
   454  	var docs []applicationOfferDoc
   455  	if err := applicationOffersCollection.Find(query).All(&docs); err != nil {
   456  		return nil, errors.Annotatef(err, "reading application %q offers", application)
   457  	}
   458  	return docs, nil
   459  }
   460  
   461  // applicationHasConnectedOffers returns true when any of the the application's
   462  // offers have connections
   463  func applicationHasConnectedOffers(st *State, application string) (bool, error) {
   464  	offers, err := applicationOffersDocs(st, application)
   465  	if err != nil {
   466  		return false, errors.Trace(err)
   467  	}
   468  	for _, offer := range offers {
   469  		connections, err := st.OfferConnections(offer.OfferUUID)
   470  		if err != nil {
   471  			return false, errors.Trace(err)
   472  		}
   473  		if len(connections) > 0 {
   474  			return true, nil
   475  		}
   476  	}
   477  	return false, nil
   478  }
   479  
   480  // removeApplicationOffersOps returns txn.Ops that will remove all offers for
   481  // the specified application. No assertions on the application or the offer
   482  // connections are made; the caller is responsible for ensuring that offer
   483  // connections have already been removed, or will be removed along with the
   484  // offers.
   485  func removeApplicationOffersOps(st *State, application string) ([]txn.Op, error) {
   486  	// Check how many offer refs there are. If there are none, there's
   487  	// nothing more to do. If there are refs, we assume that the number
   488  	// if refs is the same as what we find below. If there is a
   489  	// discrepancy, then it'll result in a transaction failure, and
   490  	// we'll retry.
   491  	countRefsOp, n, err := countApplicationOffersRefOp(st, application)
   492  	if err != nil {
   493  		return nil, errors.Trace(err)
   494  	}
   495  	if n == 0 {
   496  		return []txn.Op{countRefsOp}, nil
   497  	}
   498  	docs, err := applicationOffersDocs(st, application)
   499  	if err != nil {
   500  		return nil, errors.Trace(err)
   501  	}
   502  	var ops []txn.Op
   503  	for _, doc := range docs {
   504  		ops = append(ops, txn.Op{
   505  			C:      applicationOffersC,
   506  			Id:     doc.DocID,
   507  			Assert: txn.DocExists,
   508  			Remove: true,
   509  		})
   510  	}
   511  	offerRefCountKey := applicationOffersRefCountKey(application)
   512  	removeRefsOp := nsRefcounts.JustRemoveOp(refcountsC, offerRefCountKey, len(docs))
   513  	return append(ops, removeRefsOp), nil
   514  }
   515  
   516  var errDuplicateApplicationOffer = errors.Errorf("application offer already exists")
   517  
   518  func (s *applicationOffers) validateOfferArgs(offer crossmodel.AddApplicationOfferArgs) (err error) {
   519  	// Same rules for valid offer names apply as for applications.
   520  	if !names.IsValidApplication(offer.OfferName) {
   521  		return errors.NotValidf("offer name %q", offer.OfferName)
   522  	}
   523  	if !names.IsValidUser(offer.Owner) {
   524  		return errors.NotValidf("offer owner %q", offer.Owner)
   525  	}
   526  	for _, readUser := range offer.HasRead {
   527  		if !names.IsValidUser(readUser) {
   528  			return errors.NotValidf("offer reader %q", readUser)
   529  		}
   530  	}
   531  
   532  	// Check application and endpoints exist in state.
   533  	app, err := s.st.Application(offer.ApplicationName)
   534  	if err != nil {
   535  		return errors.Trace(err)
   536  	}
   537  
   538  	eps, err := getApplicationEndpoints(app, offer.Endpoints)
   539  	if err != nil {
   540  		return errors.Trace(err)
   541  	}
   542  	for _, ep := range eps {
   543  		if ep.Scope != charm.ScopeGlobal {
   544  			return fmt.Errorf("can only offer endpoints with global scope, provided scope %q", ep.Scope)
   545  		}
   546  	}
   547  
   548  	return nil
   549  }
   550  
   551  // AddOffer adds a new application offering to the directory.
   552  func (s *applicationOffers) AddOffer(offerArgs crossmodel.AddApplicationOfferArgs) (_ *crossmodel.ApplicationOffer, err error) {
   553  	defer errors.DeferredAnnotatef(&err, "cannot add application offer %q", offerArgs.OfferName)
   554  
   555  	if err := s.validateOfferArgs(offerArgs); err != nil {
   556  		return nil, err
   557  	}
   558  	model, err := s.st.Model()
   559  	if err != nil {
   560  		return nil, errors.Trace(err)
   561  	} else if model.Life() != Alive {
   562  		return nil, errors.Errorf("model is no longer alive")
   563  	}
   564  	uuid, err := utils.NewUUID()
   565  	if err != nil {
   566  		return nil, errors.Trace(err)
   567  	}
   568  	doc := s.makeApplicationOfferDoc(s.st, uuid.String(), offerArgs)
   569  	result, err := s.makeApplicationOffer(doc)
   570  	if err != nil {
   571  		return nil, errors.Trace(err)
   572  	}
   573  	buildTxn := func(attempt int) ([]txn.Op, error) {
   574  		// If we've tried once already and failed, check that
   575  		// model may have been destroyed.
   576  		if attempt > 0 {
   577  			if err := checkModelActive(s.st); err != nil {
   578  				return nil, errors.Trace(err)
   579  			}
   580  			_, err := s.ApplicationOffer(offerArgs.OfferName)
   581  			if err == nil {
   582  				return nil, errDuplicateApplicationOffer
   583  			}
   584  		}
   585  		incRefOp, err := incApplicationOffersRefOp(s.st, offerArgs.ApplicationName)
   586  		if err != nil {
   587  			return nil, errors.Trace(err)
   588  		}
   589  		ops := []txn.Op{
   590  			model.assertActiveOp(),
   591  			{
   592  				C:      applicationOffersC,
   593  				Id:     doc.DocID,
   594  				Assert: txn.DocMissing,
   595  				Insert: doc,
   596  			},
   597  			incRefOp,
   598  		}
   599  		return ops, nil
   600  	}
   601  	err = s.st.db().Run(buildTxn)
   602  	if err != nil {
   603  		return nil, errors.Trace(err)
   604  	}
   605  
   606  	// Ensure the owner has admin access to the offer.
   607  	offerTag := names.NewApplicationOfferTag(doc.OfferUUID)
   608  	owner := names.NewUserTag(offerArgs.Owner)
   609  	err = s.st.CreateOfferAccess(offerTag, owner, permission.AdminAccess)
   610  	if err != nil {
   611  		return nil, errors.Annotate(err, "granting admin permission to the offer owner")
   612  	}
   613  	// Add in any read access permissions.
   614  	for _, user := range offerArgs.HasRead {
   615  		readerTag := names.NewUserTag(user)
   616  		err = s.st.CreateOfferAccess(offerTag, readerTag, permission.ReadAccess)
   617  		if err != nil {
   618  			return nil, errors.Annotatef(err, "granting read permission to %q", user)
   619  		}
   620  	}
   621  	return result, nil
   622  }
   623  
   624  // UpdateOffer replaces an existing offer at the same URL.
   625  func (s *applicationOffers) UpdateOffer(offerArgs crossmodel.AddApplicationOfferArgs) (_ *crossmodel.ApplicationOffer, err error) {
   626  	defer errors.DeferredAnnotatef(&err, "cannot update application offer %q", offerArgs.OfferName)
   627  
   628  	if err := s.validateOfferArgs(offerArgs); err != nil {
   629  		return nil, err
   630  	}
   631  	model, err := s.st.Model()
   632  	if err != nil {
   633  		return nil, errors.Trace(err)
   634  	} else if model.Life() != Alive {
   635  		return nil, errors.Errorf("model is no longer alive")
   636  	}
   637  
   638  	var doc applicationOfferDoc
   639  	buildTxn := func(attempt int) ([]txn.Op, error) {
   640  		// If we've tried once already and failed, check that
   641  		// model may have been destroyed.
   642  		if attempt > 0 {
   643  			if err := checkModelActive(s.st); err != nil {
   644  				return nil, errors.Trace(err)
   645  			}
   646  		}
   647  
   648  		// Load fresh copy of the offer and setup the update document.
   649  		curOfferDoc, err := s.applicationOfferDoc(offerArgs.OfferName)
   650  		if err != nil {
   651  			// This will either be NotFound or some other error.
   652  			// In either case, we return the error.
   653  			return nil, errors.Trace(err)
   654  		}
   655  		doc = s.makeApplicationOfferDoc(s.st, curOfferDoc.OfferUUID, offerArgs)
   656  
   657  		var ops []txn.Op
   658  		if offerArgs.ApplicationName != curOfferDoc.ApplicationName {
   659  			incRefOp, err := incApplicationOffersRefOp(s.st, offerArgs.ApplicationName)
   660  			if err != nil {
   661  				return nil, errors.Trace(err)
   662  			}
   663  			decRefOp, err := decApplicationOffersRefOp(s.st, curOfferDoc.ApplicationName)
   664  			if err != nil {
   665  				return nil, errors.Trace(err)
   666  			}
   667  			ops = append(ops, incRefOp, decRefOp)
   668  		} else {
   669  			// Figure out if we are trying to remove any endpoints from the
   670  			// current offer instance.
   671  			existingEndpoints := set.NewStrings()
   672  			for _, ep := range curOfferDoc.Endpoints {
   673  				existingEndpoints.Add(ep)
   674  			}
   675  
   676  			updatedEndpoints := set.NewStrings()
   677  			for _, ep := range offerArgs.Endpoints {
   678  				updatedEndpoints.Add(ep)
   679  			}
   680  
   681  			// If the update removes any existing endpoints ensure that they
   682  			// are not currently in use and return an error if that's the
   683  			// case. This prevents users from accidentally breaking saas
   684  			// consumers.
   685  			goneEndpoints := existingEndpoints.Difference(updatedEndpoints)
   686  			if err := s.ensureEndpointsNotInUse(curOfferDoc.ApplicationName, curOfferDoc.OfferUUID, goneEndpoints); err != nil {
   687  				return nil, err
   688  			}
   689  		}
   690  
   691  		return append(ops,
   692  			model.assertActiveOp(),
   693  			txn.Op{
   694  				C:      applicationOffersC,
   695  				Id:     doc.DocID,
   696  				Assert: bson.D{{"txn-revno", curOfferDoc.TxnRevno}},
   697  				Update: bson.M{"$set": doc},
   698  			},
   699  		), nil
   700  	}
   701  	err = s.st.db().Run(buildTxn)
   702  	if err != nil {
   703  		return nil, errors.Trace(err)
   704  	}
   705  	return s.makeApplicationOffer(doc)
   706  }
   707  
   708  func (s *applicationOffers) ensureEndpointsNotInUse(appName, offerUUID string, endpoints set.Strings) error {
   709  	if len(endpoints) == 0 {
   710  		return nil
   711  	}
   712  
   713  	connections, err := s.st.OfferConnections(offerUUID)
   714  	if err != nil {
   715  		return errors.Trace(err)
   716  	}
   717  
   718  	inUse := set.NewStrings()
   719  	for _, conn := range connections {
   720  		for _, part := range strings.Fields(conn.RelationKey()) {
   721  			tokens := strings.Split(part, ":")
   722  			if len(tokens) != 2 {
   723  				return errors.New("malformed relation key")
   724  			}
   725  
   726  			if tokens[0] == appName && endpoints.Contains(tokens[1]) {
   727  				inUse.Add(tokens[1])
   728  			}
   729  		}
   730  	}
   731  
   732  	switch len(inUse) {
   733  	case 0:
   734  		return nil
   735  	case 1:
   736  		return errors.Errorf("application endpoint %q has active consumers", inUse.Values()[0])
   737  	default:
   738  		return errors.Errorf("application endpoints %q have active consumers", strings.Join(inUse.SortedValues(), ", "))
   739  	}
   740  }
   741  
   742  func (s *applicationOffers) makeApplicationOfferDoc(mb modelBackend, uuid string, offer crossmodel.AddApplicationOfferArgs) applicationOfferDoc {
   743  	doc := applicationOfferDoc{
   744  		DocID:                  mb.docID(offer.OfferName),
   745  		OfferUUID:              uuid,
   746  		OfferName:              offer.OfferName,
   747  		ApplicationName:        offer.ApplicationName,
   748  		ApplicationDescription: offer.ApplicationDescription,
   749  		Endpoints:              offer.Endpoints,
   750  	}
   751  	return doc
   752  }
   753  
   754  func (s *applicationOffers) makeFilterTerm(filterTerm crossmodel.ApplicationOfferFilter) bson.D {
   755  	var filter bson.D
   756  	if filterTerm.ApplicationName != "" {
   757  		filter = append(filter, bson.DocElem{"application-name", filterTerm.ApplicationName})
   758  	}
   759  	// We match on partial names, eg "-sql"
   760  	if filterTerm.OfferName != "" {
   761  		filter = append(filter, bson.DocElem{"offer-name", bson.D{{"$regex", fmt.Sprintf(".*%s.*", filterTerm.OfferName)}}})
   762  	}
   763  	// We match descriptions by looking for containing terms.
   764  	if filterTerm.ApplicationDescription != "" {
   765  		desc := regexp.QuoteMeta(filterTerm.ApplicationDescription)
   766  		filter = append(filter, bson.DocElem{"application-description", bson.D{{"$regex", fmt.Sprintf(".*%s.*", desc)}}})
   767  	}
   768  	return filter
   769  }
   770  
   771  // ListOffers returns the application offers matching any one of the filter terms.
   772  func (s *applicationOffers) ListOffers(filters ...crossmodel.ApplicationOfferFilter) ([]crossmodel.ApplicationOffer, error) {
   773  	applicationOffersCollection, closer := s.st.db().GetCollection(applicationOffersC)
   774  	defer closer()
   775  
   776  	var offerDocs []applicationOfferDoc
   777  	if len(filters) == 0 {
   778  		err := applicationOffersCollection.Find(nil).All(&offerDocs)
   779  		if err != nil {
   780  			return nil, errors.Trace(err)
   781  		}
   782  	}
   783  	for _, filter := range filters {
   784  		var mgoQuery bson.D
   785  		elems := s.makeFilterTerm(filter)
   786  		mgoQuery = append(mgoQuery, elems...)
   787  
   788  		var docs []applicationOfferDoc
   789  		err := applicationOffersCollection.Find(mgoQuery).All(&docs)
   790  		if err != nil {
   791  			return nil, errors.Trace(err)
   792  		}
   793  
   794  		docs, err = s.filterOffers(docs, filter)
   795  		if err != nil {
   796  			return nil, errors.Trace(err)
   797  		}
   798  		offerDocs = append(offerDocs, docs...)
   799  	}
   800  	sort.Sort(offerSlice(offerDocs))
   801  
   802  	offers := make([]crossmodel.ApplicationOffer, len(offerDocs))
   803  	for i, doc := range offerDocs {
   804  		offer, err := s.makeApplicationOffer(doc)
   805  		if err != nil {
   806  			return nil, errors.Trace(err)
   807  		}
   808  		offers[i] = *offer
   809  	}
   810  	return offers, nil
   811  }
   812  
   813  // filterOffers takes a list of offers resulting from a db query
   814  // and performs additional filtering which cannot be done via mongo.
   815  func (s *applicationOffers) filterOffers(
   816  	in []applicationOfferDoc,
   817  	filter crossmodel.ApplicationOfferFilter,
   818  ) ([]applicationOfferDoc, error) {
   819  
   820  	out, err := s.filterOffersByEndpoint(in, filter.Endpoints)
   821  	if err != nil {
   822  		return nil, errors.Trace(err)
   823  	}
   824  	out, err = s.filterOffersByConnectedUser(out, filter.ConnectedUsers)
   825  	if err != nil {
   826  		return nil, errors.Trace(err)
   827  	}
   828  	return s.filterOffersByAllowedConsumer(out, filter.AllowedConsumers)
   829  }
   830  
   831  func (s *applicationOffers) filterOffersByEndpoint(
   832  	in []applicationOfferDoc,
   833  	endpoints []crossmodel.EndpointFilterTerm,
   834  ) ([]applicationOfferDoc, error) {
   835  
   836  	if len(endpoints) == 0 {
   837  		return in, nil
   838  	}
   839  
   840  	match := func(ep Endpoint) bool {
   841  		for _, fep := range endpoints {
   842  			if fep.Interface != "" && fep.Interface == ep.Interface {
   843  				continue
   844  			}
   845  			if fep.Name != "" && fep.Name == ep.Name {
   846  				continue
   847  			}
   848  			if fep.Role != "" && fep.Role == ep.Role {
   849  				continue
   850  			}
   851  			return false
   852  		}
   853  		return true
   854  	}
   855  
   856  	var out []applicationOfferDoc
   857  	for _, doc := range in {
   858  		app, err := s.st.Application(doc.ApplicationName)
   859  		if err != nil {
   860  			return nil, errors.Trace(err)
   861  		}
   862  		for _, epName := range doc.Endpoints {
   863  			ep, err := app.Endpoint(epName)
   864  			if err != nil {
   865  				return nil, errors.Trace(err)
   866  			}
   867  			if match(ep) {
   868  				out = append(out, doc)
   869  			}
   870  		}
   871  	}
   872  	return out, nil
   873  }
   874  
   875  func (s *applicationOffers) filterOffersByConnectedUser(
   876  	in []applicationOfferDoc,
   877  	users []string,
   878  ) ([]applicationOfferDoc, error) {
   879  
   880  	if len(users) == 0 {
   881  		return in, nil
   882  	}
   883  
   884  	offerUUIDS := make(set.Strings)
   885  	for _, username := range users {
   886  		conns, err := s.st.OfferConnectionsForUser(username)
   887  		if err != nil {
   888  			return nil, errors.Trace(err)
   889  		}
   890  		for _, oc := range conns {
   891  			offerUUIDS.Add(oc.OfferUUID())
   892  		}
   893  	}
   894  	var out []applicationOfferDoc
   895  	for _, doc := range in {
   896  		if offerUUIDS.Contains(doc.OfferUUID) {
   897  			out = append(out, doc)
   898  		}
   899  	}
   900  	return out, nil
   901  }
   902  
   903  func (s *applicationOffers) filterOffersByAllowedConsumer(
   904  	in []applicationOfferDoc,
   905  	users []string,
   906  ) ([]applicationOfferDoc, error) {
   907  
   908  	if len(users) == 0 {
   909  		return in, nil
   910  	}
   911  
   912  	var out []applicationOfferDoc
   913  	for _, doc := range in {
   914  		offerUsers, err := s.st.GetOfferUsers(doc.OfferUUID)
   915  		if err != nil {
   916  			return nil, errors.Trace(err)
   917  		}
   918  		for _, username := range users {
   919  			if offerUsers[username].EqualOrGreaterOfferAccessThan(permission.ConsumeAccess) {
   920  				out = append(out, doc)
   921  				break
   922  			}
   923  		}
   924  	}
   925  	return out, nil
   926  }
   927  
   928  func (s *applicationOffers) makeApplicationOffer(doc applicationOfferDoc) (*crossmodel.ApplicationOffer, error) {
   929  	offer := &crossmodel.ApplicationOffer{
   930  		OfferName:              doc.OfferName,
   931  		OfferUUID:              doc.OfferUUID,
   932  		ApplicationName:        doc.ApplicationName,
   933  		ApplicationDescription: doc.ApplicationDescription,
   934  	}
   935  	app, err := s.st.Application(doc.ApplicationName)
   936  	if err != nil {
   937  		return nil, errors.Trace(err)
   938  	}
   939  	eps, err := getApplicationEndpoints(app, doc.Endpoints)
   940  	if err != nil {
   941  		return nil, errors.Trace(err)
   942  	}
   943  	offer.Endpoints = eps
   944  	return offer, nil
   945  }
   946  
   947  func getApplicationEndpoints(application *Application, endpointNames map[string]string) (map[string]charm.Relation, error) {
   948  	result := make(map[string]charm.Relation)
   949  	for alias, endpointName := range endpointNames {
   950  		endpoint, err := application.Endpoint(endpointName)
   951  		if err != nil {
   952  			return nil, errors.Annotatef(err, "getting relation endpoint for relation %q and application %q", endpointName, application.Name())
   953  		}
   954  		result[alias] = endpoint.Relation
   955  	}
   956  	return result, nil
   957  }
   958  
   959  type offerSlice []applicationOfferDoc
   960  
   961  func (sr offerSlice) Len() int      { return len(sr) }
   962  func (sr offerSlice) Swap(i, j int) { sr[i], sr[j] = sr[j], sr[i] }
   963  func (sr offerSlice) Less(i, j int) bool {
   964  	sr1 := sr[i]
   965  	sr2 := sr[j]
   966  	if sr1.OfferName == sr2.OfferName {
   967  		return sr1.ApplicationName < sr2.ApplicationName
   968  	}
   969  	return sr1.OfferName < sr2.OfferName
   970  }
   971  
   972  // WatchOfferStatus returns a NotifyWatcher that notifies of changes
   973  // to the offer's status.
   974  func (st *State) WatchOfferStatus(offerUUID string) (NotifyWatcher, error) {
   975  	oa := NewApplicationOffers(st)
   976  	offer, err := oa.ApplicationOfferForUUID(offerUUID)
   977  	if err != nil {
   978  		return nil, errors.Trace(err)
   979  	}
   980  	filter := func(id interface{}) bool {
   981  		k, err := st.strictLocalID(id.(string))
   982  		if err != nil {
   983  			return false
   984  		}
   985  
   986  		// Does the app name match?
   987  		if strings.HasPrefix(k, "a#") && k[2:] == offer.ApplicationName {
   988  			return true
   989  		}
   990  
   991  		// Maybe it is a status change for a unit of the app.
   992  		if !strings.HasPrefix(k, "u#") && !strings.HasSuffix(k, "#charm") {
   993  			return false
   994  		}
   995  		k = strings.TrimRight(k[2:], "#charm")
   996  		if !names.IsValidUnit(k) {
   997  			return false
   998  		}
   999  
  1000  		unitApp, _ := names.UnitApplication(k)
  1001  		return unitApp == offer.ApplicationName
  1002  	}
  1003  	return newNotifyCollWatcher(st, statusesC, filter), nil
  1004  }
  1005  
  1006  // WatchOffer returns a new NotifyWatcher watching for
  1007  // changes to the specified offer.
  1008  func (st *State) WatchOffer(offerName string) NotifyWatcher {
  1009  	filter := func(rawId interface{}) bool {
  1010  		id, err := st.strictLocalID(rawId.(string))
  1011  		if err != nil {
  1012  			return false
  1013  		}
  1014  		return offerName == id
  1015  	}
  1016  	return newNotifyCollWatcher(st, applicationOffersC, filter)
  1017  }