406
func (s *Store) putArchive(archive blobstore.ReadSeekCloser) (blobName, blobHash, blobHash256 string, size int64, err error) {
407
hash := blobstore.NewHash()
408
hash256 := sha256.New()
409
size, err = io.Copy(io.MultiWriter(hash, hash256), archive)
411
return "", "", "", 0, errgo.Notef(err, "cannot copy archive")
413
if _, err = archive.Seek(0, 0); err != nil {
414
return "", "", "", 0, errgo.Notef(err, "cannot seek in archive")
416
blobHash = fmt.Sprintf("%x", hash.Sum(nil))
417
blobName = bson.NewObjectId().Hex()
418
if err = s.BlobStore.PutUnchallenged(archive, blobName, size, blobHash); err != nil {
419
return "", "", "", 0, errgo.Notef(err, "cannot put archive into blob store")
421
return blobName, blobHash, fmt.Sprintf("%x", hash256.Sum(nil)), size, nil
424
// AddCharmWithArchive is like AddCharm but
425
// also adds the charm archive to the blob store.
426
// This method is provided principally so that
427
// tests can easily create content in the store.
429
// If purl is not nil then the charm will also be
430
// available at the promulgated url specified.
431
func (s *Store) AddCharmWithArchive(url *router.ResolvedURL, ch charm.Charm) error {
432
blobName, blobHash, blobHash256, blobSize, err := s.uploadCharmOrBundle(ch)
434
return errgo.Notef(err, "cannot upload charm")
436
return s.AddCharm(ch, AddParams{
440
BlobHash256: blobHash256,
445
// AddBundleWithArchive is like AddBundle but
446
// also adds the charm archive to the blob store.
447
// This method is provided principally so that
448
// tests can easily create content in the store.
450
// If purl is not nil then the bundle will also be
451
// available at the promulgated url specified.
452
func (s *Store) AddBundleWithArchive(url *router.ResolvedURL, b charm.Bundle) error {
453
blobName, blobHash, blobHash256, size, err := s.uploadCharmOrBundle(b)
455
return errgo.Notef(err, "cannot upload bundle")
457
return s.AddBundle(b, AddParams{
461
BlobHash256: blobHash256,
466
func (s *Store) uploadCharmOrBundle(c interface{}) (blobName, blobHash, blobHash256 string, size int64, err error) {
467
archive, err := getArchive(c)
469
return "", "", "", 0, errgo.Notef(err, "cannot get archive")
471
defer archive.Close()
472
return s.putArchive(archive)
475
413
// AddAudit adds the given entry to the audit log.
476
414
func (s *Store) AddAudit(entry audit.Entry) {
477
415
s.addAuditAtTime(entry, time.Now())
491
// AddParams holds parameters held in common between the
492
// Store.AddCharm and Store.AddBundle methods.
493
type AddParams struct {
494
// URL holds the id to be associated with the stored entity.
495
// If URL.PromulgatedRevision is not -1, the entity will
497
URL *router.ResolvedURL
499
// BlobName holds the name of the entity's archive blob.
502
// BlobHash holds the hash of the entity's archive blob.
505
// BlobHash256 holds the sha256 hash of the entity's archive blob.
508
// BlobHash holds the size of the entity's archive blob.
511
// Contents holds references to files inside the
512
// entity's archive blob.
513
Contents map[mongodoc.FileId]mongodoc.ZipFile
516
// AddCharm adds a charm entities collection with the given parameters.
517
// If p.URL cannot be used as a name for the charm then the returned
518
// error will have the cause params.ErrEntityIdNotAllowed. If the charm
519
// duplicates an existing charm then the returned error will have the
520
// cause params.ErrDuplicateUpload.
521
func (s *Store) AddCharm(c charm.Charm, p AddParams) (err error) {
522
// Strictly speaking this test is redundant, because a ResolvedURL should
523
// always be canonical, but check just in case anyway, as this is
524
// final gateway before a potentially invalid url might be stored
527
if id.Series == "bundle" || id.User == "" || id.Revision == -1 {
528
return errgo.Newf("charm added with invalid id %v", &id)
530
logger.Infof("add charm url %s; prev %d; dev %v", &id, p.URL.PromulgatedRevision, p.URL.Development)
531
entity := &mongodoc.Entity{
533
PromulgatedURL: p.URL.PromulgatedURL(),
534
BlobHash: p.BlobHash,
535
BlobHash256: p.BlobHash256,
536
BlobName: p.BlobName,
538
UploadTime: time.Now(),
540
CharmConfig: c.Config(),
541
CharmActions: c.Actions(),
542
CharmProvidedInterfaces: interfacesForRelations(c.Meta().Provides),
543
CharmRequiredInterfaces: interfacesForRelations(c.Meta().Requires),
544
Contents: p.Contents,
545
SupportedSeries: c.Meta().Series,
546
Development: p.URL.Development,
548
denormalizeEntity(entity)
550
// Check that we're not going to create a charm that duplicates
551
// the name of a bundle. This is racy, but it's the best we can
552
// do. Also check that there isn't an existing multi-series charm
553
// that would be replaced by this one.
554
entities, err := s.FindEntities(entity.BaseURL)
556
return errgo.Notef(err, "cannot check for existing entities")
558
for _, entity := range entities {
559
if entity.URL.Series == "bundle" {
560
return errgo.WithCausef(err, params.ErrEntityIdNotAllowed, "charm name duplicates bundle name %v", entity.URL)
562
if id.Series != "" && entity.URL.Series == "" {
563
return errgo.WithCausef(err, params.ErrEntityIdNotAllowed, "charm name duplicates multi-series charm name %v", entity.URL)
566
if err := s.insertEntity(entity); err != nil {
567
return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload))
572
// denormalizeEntity sets all denormalized fields in e
573
// from their associated canonical fields.
575
// It is the responsibility of the caller to set e.SupportedSeries
576
// if the entity URL does not contain a series. If the entity
577
// URL *does* contain a series, e.SupportedSeries will
580
// This is exported for the purposes of tests that
581
// need to create directly into the database.
582
func denormalizeEntity(e *mongodoc.Entity) {
583
e.BaseURL = baseURL(e.URL)
586
e.Revision = e.URL.Revision
587
e.Series = e.URL.Series
588
if e.URL.Series != "" {
589
if e.URL.Series == "bundle" {
590
e.SupportedSeries = nil
592
e.SupportedSeries = []string{e.URL.Series}
595
if e.PromulgatedURL == nil {
596
e.PromulgatedRevision = -1
598
e.PromulgatedRevision = e.PromulgatedURL.Revision
602
var everyonePerm = []string{params.Everyone}
604
func (s *Store) insertEntity(entity *mongodoc.Entity) (err error) {
605
// Add the base entity to the database.
606
perms := []string{entity.User}
607
acls := mongodoc.ACL{
611
baseEntity := &mongodoc.BaseEntity{
617
DevelopmentACLs: acls,
618
Promulgated: entity.PromulgatedURL != nil,
620
err = s.DB.BaseEntities().Insert(baseEntity)
621
if err != nil && !mgo.IsDup(err) {
622
return errgo.Notef(err, "cannot insert base entity")
625
// Add the entity to the database.
626
err = s.DB.Entities().Insert(entity)
628
return params.ErrDuplicateUpload
631
return errgo.Notef(err, "cannot insert entity")
633
// Ensure that if anything fails after this, that we delete
634
// the entity, otherwise we will be left in an internally
635
// inconsistent state.
638
if err := s.DB.Entities().RemoveId(entity.URL); err != nil {
639
logger.Errorf("cannot remove entity after elastic search failure: %v", err)
643
// Add entity to ElasticSearch.
644
if err := s.UpdateSearch(EntityResolvedURL(entity)); err != nil {
645
return errgo.Notef(err, "cannot index %s to ElasticSearch", entity.URL)
650
// FindEntity finds the entity in the store with the given URL,
651
// which must be fully qualified. If any fields are specified,
652
// only those fields will be populated in the returned entities.
653
// If the given URL has no user then it is assumed to be a
654
// promulgated entity.
655
func (s *Store) FindEntity(url *router.ResolvedURL, fields ...string) (*mongodoc.Entity, error) {
656
entities, err := s.FindEntities(url.UserOwnedURL(), fields...)
429
// FindEntity finds the entity in the store with the given URL, which
430
// must be fully qualified. If the given URL has no user then it is
431
// assumed to be a promulgated entity. If fields is not nil, only its
432
// fields will be populated in the returned entities.
433
func (s *Store) FindEntity(url *router.ResolvedURL, fields map[string]int) (*mongodoc.Entity, error) {
434
q := s.DB.Entities().Find(bson.D{{"_id", &url.URL}})
438
var entity mongodoc.Entity
439
err := q.One(&entity)
441
if err == mgo.ErrNotFound {
442
return nil, errgo.WithCausef(nil, params.ErrNotFound, "entity not found")
658
444
return nil, errgo.Mask(err)
660
if len(entities) == 0 {
661
return nil, errgo.WithCausef(nil, params.ErrNotFound, "entity not found")
663
// The URL is guaranteed to be fully qualified so we'll always
664
// get exactly one result.
665
return entities[0], nil
668
449
// FindEntities finds all entities in the store matching the given URL.
669
// If any fields are specified, only those fields will be
670
// populated in the returned entities. If the given URL has no user then
671
// only promulgated entities will be queried. If the given URL channel does
672
// not represent an entity under development then only published entities
674
func (s *Store) FindEntities(url *charm.URL, fields ...string) ([]*mongodoc.Entity, error) {
675
query := selectFields(s.EntitiesQuery(url), fields)
450
// If the given URL has no user then only promulgated entities will be
451
// queried. If the given URL channel does not represent an entity under
452
// development then only published entities will be queried. If fields
453
// is not nil, only its fields will be populated in the returned
455
func (s *Store) FindEntities(url *charm.URL, fields map[string]int) ([]*mongodoc.Entity, error) {
456
query := s.EntitiesQuery(url)
458
query = query.Select(fields)
676
460
var docs []*mongodoc.Entity
677
461
err := query.All(&docs)
684
468
// FindBestEntity finds the entity that provides the preferred match to
685
// the given URL. If any fields are specified, only those fields will be
686
// populated in the returned entities. If the given URL has no user then
687
// only promulgated entities will be queried.
688
func (s *Store) FindBestEntity(url *charm.URL, fields ...string) (*mongodoc.Entity, error) {
469
// the given URL, on the given channel. If the given URL has no user
470
// then only promulgated entities will be queried. If fields is not nil,
471
// only those fields will be populated in the returned entities.
473
// If the URL contains a revision then it is assumed to be fully formed
474
// and refer to a single entity; the channel is ignored.
476
// If the URL does not contain a revision then the channel is searched
477
// for the best match, here NoChannel will be treated as
478
// params.StableChannel.
479
func (s *Store) FindBestEntity(url *charm.URL, channel params.Channel, fields map[string]int) (*mongodoc.Entity, error) {
690
481
// Make sure we have all the fields we need to make a decision.
691
fields = append(fields, "_id", "promulgated-url", "promulgated-revision", "series", "revision")
693
entities, err := s.FindEntities(url, fields...)
482
// TODO this would be more efficient if we used bitmasks for field selection.
483
nfields := map[string]int{
485
"promulgated-url": 1,
486
"promulgated-revision": 1,
492
for f := range fields {
497
if url.Revision != -1 {
498
// If the URL contains a revision, then it refers to a single entity.
499
entity, err := s.findSingleEntity(url, fields)
500
if errgo.Cause(err) == params.ErrNotFound {
501
return nil, errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", url)
502
} else if err != nil {
503
return nil, errgo.Mask(err)
505
// If a channel was specified make sure the entity is in that channel.
506
// This is crucial because if we don't do this, then the user could choose
507
// to use any chosen set of ACLs against any entity.
509
case params.StableChannel:
511
return nil, errgo.WithCausef(nil, params.ErrNotFound, "%s not found in stable channel", url)
513
case params.DevelopmentChannel:
514
if !entity.Development {
515
return nil, errgo.WithCausef(nil, params.ErrNotFound, "%s not found in development channel", url)
522
case params.UnpublishedChannel:
523
return s.findUnpublishedEntity(url, fields)
524
case params.NoChannel:
525
channel = params.StableChannel
528
return s.findEntityInChannel(url, channel, fields)
532
// findSingleEntity returns the entity referred to by URL. It is expected
533
// that the URL refers to only one entity and is fully formed. The url may
534
// refer to either a user-owned or promulgated charm name.
535
func (s *Store) findSingleEntity(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
536
query := s.EntitiesQuery(url)
538
query = query.Select(fields)
540
var entity mongodoc.Entity
541
err := query.One(&entity)
545
if err == mgo.ErrNotFound {
546
return nil, errgo.WithCausef(err, params.ErrNotFound, "no matching charm or bundle for %s", url)
548
return nil, errgo.Notef(err, "cannot find entities matching %s", url)
551
// findEntityInChannel attempts to find an entity on the given channel. The
552
// base entity for URL is retrieved and the series with the best match to
553
// URL.Series is used as the resolved entity.
554
func (s *Store) findEntityInChannel(url *charm.URL, ch params.Channel, fields map[string]int) (*mongodoc.Entity, error) {
555
baseEntity, err := s.FindBaseEntity(url, map[string]int{
557
"channelentities": 1,
559
if errgo.Cause(err) == params.ErrNotFound {
560
return nil, errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", url)
561
} else if err != nil {
562
return nil, errgo.Mask(err)
564
var entityURL *charm.URL
565
if url.Series == "" {
566
for _, u := range baseEntity.ChannelEntities[ch] {
567
if entityURL == nil || seriesScore[u.Series] > seriesScore[entityURL.Series] {
572
entityURL = baseEntity.ChannelEntities[ch][url.Series]
574
if entityURL == nil {
575
return nil, errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", url)
577
return s.findSingleEntity(entityURL, fields)
580
// findUnpublishedEntity attempts to find an entity on the unpublished
581
// channel. This searches all entities in the store for the best match to
583
func (s *Store) findUnpublishedEntity(url *charm.URL, fields map[string]int) (*mongodoc.Entity, error) {
584
entities, err := s.FindEntities(url, fields)
695
586
return nil, errgo.Mask(err)
697
588
if len(entities) == 0 {
698
return nil, errgo.WithCausef(nil, params.ErrNotFound, "entity not found")
589
return nil, errgo.WithCausef(nil, params.ErrNotFound, "no matching charm or bundle for %s", url)
700
591
best := entities[0]
701
592
for _, e := range entities {
837
// SetDevelopment sets whether the entity corresponding to the given URL will
838
// be only available in its development version (in essence, not published).
839
func (s *Store) SetDevelopment(url *router.ResolvedURL, development bool) error {
840
if err := s.UpdateEntity(url, bson.D{{
841
"$set", bson.D{{"development", development}},
843
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
846
// If the entity is published, update the search index.
848
rurl.Development = development
849
if err := s.UpdateSearch(&rurl); err != nil {
850
return errgo.Notef(err, "cannot update search entities for %q", rurl)
739
// Publish assigns channels to the entity corresponding to the given URL.
740
// An error is returned if no channels are provided. For the time being,
741
// the only supported channels are "development" and "stable".
742
func (s *Store) Publish(url *router.ResolvedURL, channels ...params.Channel) error {
743
var updateSearch bool
744
// Validate channels.
745
actual := make([]params.Channel, 0, len(channels))
746
for _, c := range channels {
748
case params.StableChannel:
751
case params.DevelopmentChannel:
752
actual = append(actual, c)
755
numChannels := len(actual)
756
if numChannels == 0 {
757
return errgo.Newf("cannot update %q: no channels provided", url)
760
// Update the entity.
761
update := make(bson.D, numChannels)
762
for i, c := range actual {
763
update[i] = bson.DocElem{string(c), true}
765
if err := s.UpdateEntity(url, bson.D{{"$set", update}}); err != nil {
766
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
769
// Update the base entity.
770
entity, err := s.FindEntity(url, FieldSelector("series", "supportedseries"))
772
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
774
series := entity.SupportedSeries
775
numSeries := len(series)
777
series = []string{entity.Series}
780
update = make(bson.D, 0, numChannels*numSeries)
781
for _, c := range actual {
782
for _, s := range series {
783
update = append(update, bson.DocElem{fmt.Sprintf("channelentities.%s.%s", c, s), entity.URL})
786
if err := s.UpdateBaseEntity(url, bson.D{{"$set", update}}); err != nil {
787
return errgo.Mask(err)
794
// Add entity to ElasticSearch.
795
if err := s.UpdateSearch(url); err != nil {
796
return errgo.Notef(err, "cannot index %s to ElasticSearch", url)
1003
func interfacesForRelations(rels map[string]charm.Relation) []string {
1004
// Eliminate duplicates by storing interface names into a map.
1005
interfaces := make(map[string]bool)
1006
for _, rel := range rels {
1007
interfaces[rel.Interface] = true
1009
result := make([]string, 0, len(interfaces))
1010
for iface := range interfaces {
1011
result = append(result, iface)
1016
func baseURL(url *charm.URL) *charm.URL {
1018
newURL.Revision = -1
1024
var errNotImplemented = errgo.Newf("not implemented")
1026
// AddBundle adds a bundle to the entities collection with the given
1027
// parameters. If p.URL cannot be used as a name for the bundle then the
1028
// returned error will have the cause params.ErrEntityIdNotAllowed. If
1029
// the bundle duplicates an existing bundle then the returned error will
1030
// have the cause params.ErrDuplicateUpload.
1031
func (s *Store) AddBundle(b charm.Bundle, p AddParams) error {
1032
// Strictly speaking this test is redundant, because a ResolvedURL should
1033
// always be canonical, but check just in case anyway, as this is
1034
// final gateway before a potentially invalid url might be stored
1036
if p.URL.URL.Series != "bundle" || p.URL.URL.User == "" || p.URL.URL.Revision == -1 || p.URL.URL.Series == "" {
1037
return errgo.Newf("bundle added with invalid id %v", p.URL)
1039
bundleData := b.Data()
1040
urls, err := bundleCharms(bundleData)
1042
return errgo.Mask(err)
1044
entity := &mongodoc.Entity{
1046
BlobHash: p.BlobHash,
1047
BlobHash256: p.BlobHash256,
1048
BlobName: p.BlobName,
1050
UploadTime: time.Now(),
1051
BundleData: bundleData,
1052
BundleUnitCount: newInt(bundleUnitCount(bundleData)),
1053
BundleMachineCount: newInt(bundleMachineCount(bundleData)),
1054
BundleReadMe: b.ReadMe(),
1056
Contents: p.Contents,
1057
PromulgatedURL: p.URL.PromulgatedURL(),
1058
Development: p.URL.Development,
1060
denormalizeEntity(entity)
1062
// Check that we're not going to create a bundle that duplicates
1063
// the name of a charm. This is racy, but it's the best we can do.
1064
entities, err := s.FindEntities(entity.BaseURL)
1066
return errgo.Notef(err, "cannot check for existing entities")
1068
for _, entity := range entities {
1069
if entity.URL.Series != "bundle" {
1070
return errgo.WithCausef(err, params.ErrEntityIdNotAllowed, "bundle name duplicates charm name %s", entity.URL)
1073
if err := s.insertEntity(entity); err != nil {
1074
return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload))
1079
// OpenBlob opens a blob given its entity id; it returns the blob's
1080
// data source, its size and its hash. It returns a params.ErrNotFound
1081
// error if the entity does not exist.
1082
func (s *Store) OpenBlob(id *router.ResolvedURL) (r blobstore.ReadSeekCloser, size int64, hash string, err error) {
1083
blobName, hash, err := s.BlobNameAndHash(id)
1085
return nil, 0, "", errgo.Mask(err, errgo.Is(params.ErrNotFound))
1087
r, size, err = s.BlobStore.Open(blobName)
1089
return nil, 0, "", errgo.Notef(err, "cannot open archive data for %s", id)
1091
return r, size, hash, nil
1094
// BlobNameAndHash returns the name that is used to store the blob
1095
// for the entity with the given id and its hash. It returns a params.ErrNotFound
1096
// error if the entity does not exist.
1097
func (s *Store) BlobNameAndHash(id *router.ResolvedURL) (name, hash string, err error) {
1098
entity, err := s.FindEntity(id, "blobname", "blobhash")
1100
if errgo.Cause(err) == params.ErrNotFound {
1101
return "", "", errgo.WithCausef(nil, params.ErrNotFound, "entity not found")
1103
return "", "", errgo.Notef(err, "cannot get %s", id)
1105
return entity.BlobName, entity.BlobHash, nil
1108
// OpenCachedBlobFile opens a file from the given entity's archive blob.
1109
// The file is identified by the provided fileId. If the file has not
1110
// previously been opened on this entity, the isFile function will be
1111
// used to determine which file in the zip file to use. The result will
1112
// be cached for the next time.
1114
// When retrieving the entity, at least the BlobName and
1115
// Contents fields must be populated.
1116
func (s *Store) OpenCachedBlobFile(
1117
entity *mongodoc.Entity,
1118
fileId mongodoc.FileId,
1119
isFile func(f *zip.File) bool,
1120
) (_ io.ReadCloser, err error) {
1121
if entity.BlobName == "" {
1122
// We'd like to check that the Contents field was populated
1123
// here but we can't because it doesn't necessarily
1124
// exist in the entity.
1125
return nil, errgo.New("provided entity does not have required fields")
1127
zipf, ok := entity.Contents[fileId]
1128
if ok && !zipf.IsValid() {
1129
return nil, errgo.WithCausef(nil, params.ErrNotFound, "")
1131
blob, size, err := s.BlobStore.Open(entity.BlobName)
1133
return nil, errgo.Notef(err, "cannot open archive blob")
1136
// When there's an error, we want to close
1137
// the blob, otherwise we need to keep the blob
1138
// open because it's used by the returned Reader.
1144
// We haven't already searched the archive for the icon,
1145
// so find its archive now.
1146
zipf, err = s.findZipFile(blob, size, isFile)
1147
if err != nil && errgo.Cause(err) != params.ErrNotFound {
1148
return nil, errgo.Mask(err)
1151
// We update the content entry regardless of whether we've
1152
// found a file, so that the next time that serveIcon is called
1153
// it can know that we've already looked.
1154
err = s.DB.Entities().UpdateId(
1157
bson.D{{"contents." + string(fileId), zipf}},
1161
return nil, errgo.Notef(err, "cannot update %q", entity.URL)
1163
if !zipf.IsValid() {
1164
// We searched for the file and didn't find it.
1165
return nil, errgo.WithCausef(nil, params.ErrNotFound, "")
1168
// We know where the icon is stored. Now serve it up.
1169
r, err := ZipFileReader(blob, zipf)
1171
return nil, errgo.Notef(err, "cannot make zip file reader")
1173
// We return a ReadCloser that reads from the newly created
1174
// zip file reader, but when closed, will close the originally
1182
func (s *Store) findZipFile(blob io.ReadSeeker, size int64, isFile func(f *zip.File) bool) (mongodoc.ZipFile, error) {
1183
zipReader, err := zip.NewReader(&readerAtSeeker{blob}, size)
1185
return mongodoc.ZipFile{}, errgo.Notef(err, "cannot read archive data")
1187
for _, f := range zipReader.File {
1189
return NewZipFile(f)
1192
return mongodoc.ZipFile{}, params.ErrNotFound
1195
// SetPerms sets the permissions for the base entity with
1196
// the given id for "which" operations ("read" or "write")
1197
// to the given ACL. This is mostly provided for testing.
948
// SetPerms sets the ACL specified by which for the base entity with the
949
// given id. The which parameter is in the form "[channel].operation",
950
// where channel, if specified, is one of "development" or "stable" and
951
// operation is one of "read" or "write". If which does not specify a
952
// channel then the unpublished ACL is updated. This is only provided for
1198
954
func (s *Store) SetPerms(id *charm.URL, which string, acl ...string) error {
1200
if id.Channel == charm.DevelopmentChannel {
1201
field = "developmentacls"
1203
return s.DB.BaseEntities().UpdateId(baseURL(id), bson.D{{"$set",
1204
bson.D{{field + "." + which, acl}},
955
return s.DB.BaseEntities().UpdateId(mongodoc.BaseURL(id), bson.D{{"$set",
956
bson.D{{"channelacls." + which, acl}},
1208
func newInt(x int) *int {
1212
// bundleUnitCount returns the number of units created by the bundle.
1213
func bundleUnitCount(b *charm.BundleData) int {
1215
for _, service := range b.Services {
1216
count += service.NumUnits
1221
// bundleMachineCount returns the number of machines
1222
// that will be created or used by the bundle.
1223
func bundleMachineCount(b *charm.BundleData) int {
1224
count := len(b.Machines)
1225
for _, service := range b.Services {
1226
// The default placement is "new".
1227
placement := &charm.UnitPlacement{
1230
// Check for "new" placements, which means a new machine
1232
for _, location := range service.To {
1234
placement, err = charm.ParsePlacement(location)
1236
// Ignore invalid placements - a bundle should always
1237
// be verified before adding to the charm store so this
1238
// should never happen in practice.
1241
if placement.Machine == "new" {
1245
// If there are less elements in To than NumUnits, the last placement
1246
// element is replicated. For this reason, if the last element is
1247
// "new", we need to add more machines.
1248
if placement != nil && placement.Machine == "new" {
1249
count += service.NumUnits - len(service.To)
1255
// bundleCharms returns all the charm URLs used by a bundle,
1256
// without duplicates.
1257
func bundleCharms(data *charm.BundleData) ([]*charm.URL, error) {
1258
// Use a map to de-duplicate the URL list: a bundle can include services
1259
// deployed by the same charm.
1260
urlMap := make(map[string]*charm.URL)
1261
for _, service := range data.Services {
1262
url, err := charm.ParseURL(service.Charm)
1264
return nil, errgo.Mask(err)
1266
urlMap[url.String()] = url
1267
// Also add the corresponding base URL.
1268
base := baseURL(url)
1269
urlMap[base.String()] = base
1271
urls := make([]*charm.URL, 0, len(urlMap))
1272
for _, url := range urlMap {
1273
urls = append(urls, url)
1278
960
// MatchingInterfacesQuery returns a mongo query
1279
961
// that will find any charms that require any interfaces
1280
962
// in the required slice or provide any interfaces in the
1281
963
// provided slice.
1283
// Development charms are never matched.
1284
// TODO do we actually want to match dev charms here?
1285
964
func (s *Store) MatchingInterfacesQuery(required, provided []string) *mgo.Query {
1286
965
return s.DB.Entities().Find(bson.D{{
1287
"development", false,
1289
966
"$or", []bson.D{{{
1290
967
"charmrequiredinterfaces", bson.D{{
1291
968
"$elemMatch", bson.D{{
1527
1244
return sort, nil
1530
// List lists the store for the given ListParams.
1531
// It returns a ListResult containing the results of the list.
1532
func (store *Store) List(sp SearchParams) (ListResult, error) {
1247
// ListQuery holds a list query from which an iterator
1249
type ListQuery struct {
1251
filters map[string]interface{}
1255
// ListQuery lists entities in the store that conform to the
1256
// given search paramerters. It returns a ListQuery
1257
// that can be used to iterate through the list.
1258
func (store *Store) ListQuery(sp SearchParams) (*ListQuery, error) {
1533
1259
filters, sort, err := prepareList(sp)
1534
1260
if err != nil {
1535
return ListResult{}, errgo.Mask(err)
1537
q := []bson.M{{"$match": filters}}
1538
q = append(q, bson.M{"$sort": bson.D{{"revision", 1}}})
1542
"$concat": []interface{}{
1546
"$cond": []string{"$development", "true", "false"},
1550
"promulgated-url": bson.M{"$last": "$promulgated-url"},
1551
"development": bson.M{"$last": "$development"},
1552
"name": bson.M{"$last": "$name"},
1553
"user": bson.M{"$last": "$user"},
1554
"series": bson.M{"$last": "$series"},
1555
"url": bson.M{"$last": "$_id"},
1557
group := bson.M{"$group": d}
1558
q = append(q, group)
1562
"development": "$development",
1565
"series": "$series",
1566
"promulgated-url": "$promulgated-url",
1569
q = append(q, project)
1571
q = append(q, bson.M{
1572
"$sort": bson.D{{"_id", 1}},
1575
q = append(q, bson.M{"$sort": sort})
1578
pipe := store.DB.Entities().Pipe(q)
1580
Results: make([]*router.ResolvedURL, 0),
1582
var entity mongodoc.Entity
1584
for iter.Next(&entity) {
1585
r.Results = append(r.Results, EntityResolvedURL(&entity))
1587
if err := iter.Close(); err != nil {
1588
return ListResult{}, errgo.Mask(err)
1261
return nil, errgo.Mask(err)
1270
func (lq *ListQuery) Iter(fields map[string]int) *mgo.Iter {
1271
qfields := FieldSelector(
1278
for f := range fields {
1281
// _id and url have special treatment.
1282
delete(qfields, "_id")
1283
delete(qfields, "url")
1285
group := make(bson.D, 0, 2+len(qfields))
1286
group = append(group, bson.DocElem{"_id", bson.D{{
1287
"$concat", []interface{}{
1291
"$cond", []string{"$development", "true", "false"},
1295
group = append(group, bson.DocElem{"url", bson.D{{"$last", "$_id"}}})
1296
for field := range qfields {
1297
group = append(group, bson.DocElem{field, bson.D{{"$last", "$" + field}}})
1300
project := make(bson.D, 0, len(qfields)+1)
1301
project = append(project, bson.DocElem{"_id", "$url"})
1302
for f := range qfields {
1303
project = append(project, bson.DocElem{f, "$" + f})
1307
{{"$match", lq.filters}},
1308
{{"$sort", bson.D{{"revision", 1}}}},
1309
{{"$group", group}},
1310
{{"$project", project}},
1311
{{"$sort", lq.sort}},
1313
return lq.store.DB.Entities().Pipe(q).Iter()
1593
1316
// SynchroniseElasticsearch creates new indexes in elasticsearch