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 }