1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package charmstore // import "gopkg.in/juju/charmstore.v5-unstable/internal/charmstore"
18
jujuzip "github.com/juju/zip"
20
"gopkg.in/juju/charm.v6-unstable"
21
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
23
"gopkg.in/mgo.v2/bson"
26
"gopkg.in/juju/charmstore.v5-unstable/internal/blobstore"
27
"gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
28
"gopkg.in/juju/charmstore.v5-unstable/internal/router"
29
"gopkg.in/juju/charmstore.v5-unstable/internal/series"
32
// addParams holds parameters held in common between the
33
// Store.addCharm and Store.addBundle methods.
34
type addParams struct {
35
// url holds the id to be associated with the stored entity.
36
// If URL.PromulgatedRevision is not -1, the entity will
38
url *router.ResolvedURL
40
// blobName holds the name of the entity's archive blob.
43
// blobHash holds the hash of the entity's archive blob.
46
// preV5BlobHash holds the hash of the entity's archive blob for
47
// pre-v5 compatibility purposes.
50
// preV5BlobHash256 holds the SHA256 hash of the entity's archive blob for
51
// pre-v5 compatibility purposes.
52
preV5BlobHash256 string
54
// preV5BlobSize holds the size of the entity's archive blob for
55
// pre-v5 compatibility purposes.
58
// blobHash256 holds the sha256 hash of the entity's archive blob.
61
// bobSize holds the size of the entity's archive blob.
64
// chans holds the channels to associate with the entity.
65
chans []params.Channel
68
// AddCharmWithArchive adds the given charm, which must
69
// be either a *charm.CharmDir or implement ArchiverTo,
70
// to the charmstore under the given URL.
72
// This method is provided for testing purposes only.
73
func (s *Store) AddCharmWithArchive(url *router.ResolvedURL, ch charm.Charm) error {
74
return s.AddEntityWithArchive(url, ch)
77
// AddBundleWithArchive adds the given bundle, which must
78
// be either a *charm.BundleDir or implement ArchiverTo,
79
// to the charmstore under the given URL.
81
// This method is provided for testing purposes only.
82
func (s *Store) AddBundleWithArchive(url *router.ResolvedURL, b charm.Bundle) error {
83
return s.AddEntityWithArchive(url, b)
86
// AddEntityWithArchive provides the implementation for
87
// both AddCharmWithArchive and AddBundleWithArchive.
88
// It accepts charm.Charm or charm.Bundle implementations
89
// defined in the charm package, and any that implement
91
func (s *Store) AddEntityWithArchive(url *router.ResolvedURL, archive interface{}) error {
92
blob, err := getArchive(archive)
94
return errgo.Notef(err, "cannot get archive")
97
hash := blobstore.NewHash()
98
size, err := io.Copy(hash, blob)
100
return errgo.Notef(err, "cannot copy archive")
102
if _, err := blob.Seek(0, 0); err != nil {
103
return errgo.Notef(err, "cannot seek to start of archive")
105
if err := s.UploadEntity(url, blob, fmt.Sprintf("%x", hash.Sum(nil)), size, nil); err != nil {
106
return errgo.Mask(err, errgo.Any)
111
// UploadEntity reads the given blob, which should have the given hash
112
// and size, and uploads it to the charm store, associating it with
113
// the given channels (without actually making it current in any of them).
115
// The following error causes may be returned:
116
// params.ErrDuplicateUpload if the URL duplicates an existing entity.
117
// params.ErrEntityIdNotAllowed if the id may not be created.
118
// params.ErrInvalidEntity if the provided blob is invalid.
119
func (s *Store) UploadEntity(url *router.ResolvedURL, blob io.Reader, blobHash string, size int64, chans []params.Channel) error {
120
// Strictly speaking these tests are redundant, because a ResolvedURL should
121
// always be canonical, but check just in case anyway, as this is
122
// final gateway before a potentially invalid url might be stored
124
if url.URL.User == "" {
125
return errgo.WithCausef(nil, params.ErrEntityIdNotAllowed, "entity id does not specify user")
127
if url.URL.Revision == -1 {
128
return errgo.WithCausef(nil, params.ErrEntityIdNotAllowed, "entity id does not specify revision")
130
blobName, blobHash256, err := s.putArchive(blob, size, blobHash)
132
return errgo.Mask(err)
134
r, _, err := s.BlobStore.Open(blobName)
136
return errgo.Notef(err, "cannot open newly created blob")
139
if err := s.addEntityFromReader(url, r, blobName, blobHash, blobHash256, size, chans); err != nil {
140
if err1 := s.BlobStore.Remove(blobName); err1 != nil {
141
logger.Errorf("cannot remove blob %s after error: %v", blobName, err1)
143
return errgo.Mask(err,
144
errgo.Is(params.ErrDuplicateUpload),
145
errgo.Is(params.ErrEntityIdNotAllowed),
146
errgo.Is(params.ErrInvalidEntity),
152
// putArchive reads the charm or bundle archive from the given reader and
153
// puts into the blob store. The archiveSize and hash must holds the length
154
// of the blob content and its SHA384 hash respectively.
155
func (s *Store) putArchive(blob io.Reader, blobSize int64, hash string) (blobName, blobHash256 string, err error) {
156
name := bson.NewObjectId().Hex()
158
// Calculate the SHA256 hash while uploading the blob in the blob store.
159
hash256 := sha256.New()
160
blob = io.TeeReader(blob, hash256)
162
// Upload the actual blob, and make sure that it is removed
164
err = s.BlobStore.PutUnchallenged(blob, name, blobSize, hash)
166
// TODO return error with ErrInvalidEntity cause when
167
// there's a hash or size mismatch.
168
return "", "", errgo.Notef(err, "cannot put archive blob")
170
return name, fmt.Sprintf("%x", hash256.Sum(nil)), nil
173
// addEntityFromReader adds the entity represented by the contents
174
// of the given reader, associating it with the given id.
175
func (s *Store) addEntityFromReader(id *router.ResolvedURL, r io.ReadSeeker, blobName, hash, hash256 string, blobSize int64, chans []params.Channel) error {
180
blobHash256: hash256,
183
preV5BlobHash256: hash256,
184
preV5BlobSize: blobSize,
187
if id.URL.Series == "bundle" {
188
b, err := s.newBundle(id, r, blobSize)
190
return errgo.Mask(err, errgo.Is(params.ErrInvalidEntity), errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
192
if err := s.addBundle(b, p); err != nil {
193
return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
197
ch, err := s.newCharm(id, r, blobSize)
199
return errgo.Mask(err, errgo.Is(params.ErrInvalidEntity), errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
201
if len(ch.Meta().Series) > 0 {
202
if _, err := r.Seek(0, 0); err != nil {
203
return errgo.Notef(err, "cannot seek to start of archive")
205
logger.Infof("adding pre-v5 compat blob for %#v", id)
206
info, err := addPreV5CompatibilityHackBlob(s.BlobStore, r, p.blobName, p.blobSize)
208
return errgo.Notef(err, "cannot add pre-v5 compatibility blob")
210
p.preV5BlobHash = info.hash
211
p.preV5BlobHash256 = info.hash256
212
p.preV5BlobSize = info.size
214
err = s.addCharm(ch, p)
215
if err != nil && len(ch.Meta().Series) > 0 {
216
// We added a compatibility blob so we need to remove it.
217
compatBlobName := preV5CompatibilityBlobName(p.blobName)
218
if err1 := s.BlobStore.Remove(compatBlobName); err1 != nil {
219
logger.Errorf("cannot remove blob %s after error: %v", compatBlobName, err1)
223
return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
228
type preV5CompatibilityHackBlobInfo struct {
234
// addPreV5CompatibilityHackBlob adds a second blob to the blob store that
235
// contains a suffix to the zipped charm archive file that updates the zip
236
// index to point to an updated version of metadata.yaml that does
237
// not have a series field. The original blob is held in r.
238
// It updates the fields in p accordingly.
240
// We do this because earlier versions of the charm package have a version
241
// of the series field that holds a single string rather than a slice of string
242
// so will fail when reading the new slice-of-string form, and we
243
// don't want to change the field name from "series".
244
func addPreV5CompatibilityHackBlob(blobStore *blobstore.Store, r io.ReadSeeker, blobName string, blobSize int64) (*preV5CompatibilityHackBlobInfo, error) {
245
readerAt := ReaderAtSeeker(r)
246
z, err := jujuzip.NewReader(readerAt, blobSize)
248
return nil, errgo.Notef(err, "cannot open charm archive")
250
var metadataf *jujuzip.File
251
for _, f := range z.File {
252
if f.Name == "metadata.yaml" {
257
if metadataf == nil {
258
return nil, errgo.New("no metadata.yaml file found")
260
fr, err := metadataf.Open()
262
return nil, errgo.Notef(err, "cannot open metadata.yaml from archive")
265
data, err := removeSeriesField(fr)
267
return nil, errgo.Notef(err, "cannot remove series field from metadata")
269
var appendedBlob bytes.Buffer
270
zw := z.Append(&appendedBlob)
271
updatedf := metadataf.FileHeader // Work around invalid duplicate FileHeader issue.
272
zwf, err := zw.CreateHeader(&updatedf)
274
return nil, errgo.Notef(err, "cannot create appended metadata entry")
276
if _, err := zwf.Write(data); err != nil {
277
return nil, errgo.Notef(err, "cannot write appended metadata data")
279
if err := zw.Close(); err != nil {
280
return nil, errgo.Notef(err, "cannot close zip file")
282
data = appendedBlob.Bytes()
283
sha384sum := sha512.Sum384(data)
285
err = blobStore.PutUnchallenged(&appendedBlob, preV5CompatibilityBlobName(blobName), int64(len(data)), fmt.Sprintf("%x", sha384sum[:]))
287
return nil, errgo.Notef(err, "cannot put archive blob")
290
sha384w := sha512.New384()
291
sha256w := sha256.New()
292
hashw := io.MultiWriter(sha384w, sha256w)
293
if _, err := r.Seek(0, 0); err != nil {
294
return nil, errgo.Notef(err, "cannnot seek to start of blob")
296
if _, err := io.Copy(hashw, r); err != nil {
297
return nil, errgo.Notef(err, "cannot recalculate blob checksum")
300
return &preV5CompatibilityHackBlobInfo{
301
size: blobSize + int64(len(data)),
302
hash256: fmt.Sprintf("%x", sha256w.Sum(nil)),
303
hash: fmt.Sprintf("%x", sha384w.Sum(nil)),
307
// preV5CompatibilityBlobName returns the name of the zip file suffix used
308
// to overwrite the metadata.yaml file for pre-v5 compatibility purposes.
309
func preV5CompatibilityBlobName(blobName string) string {
310
return blobName + ".pre-v5-suffix"
313
func removeSeriesField(r io.Reader) ([]byte, error) {
314
data, err := ioutil.ReadAll(r)
316
return nil, errgo.Mask(err)
318
var meta map[string]interface{}
319
if err := yaml.Unmarshal(data, &meta); err != nil {
320
return nil, errgo.Notef(err, "cannot unmarshal metadata.yaml")
322
delete(meta, "series")
323
data, err = yaml.Marshal(meta)
325
return nil, errgo.Notef(err, "cannot re-marshal metadata.yaml")
330
// newCharm returns a new charm implementation from the archive blob
331
// read from r, that should have the given size and will
332
// be named with the given id.
334
// The charm is checked for validity before returning.
335
func (s *Store) newCharm(id *router.ResolvedURL, r io.ReadSeeker, blobSize int64) (charm.Charm, error) {
336
readerAt := ReaderAtSeeker(r)
337
ch, err := charm.ReadCharmArchiveFromReader(readerAt, blobSize)
339
return nil, zipReadError(err, "cannot read charm archive")
341
if err := checkCharmIsValid(ch); err != nil {
342
return nil, errgo.Mask(err, errgo.Is(params.ErrInvalidEntity))
344
if err := checkIdAllowed(id, ch); err != nil {
345
return nil, errgo.Mask(err, errgo.Is(params.ErrEntityIdNotAllowed))
350
func checkCharmIsValid(ch charm.Charm) error {
352
for _, rels := range []map[string]charm.Relation{m.Provides, m.Requires, m.Peers} {
353
if err := checkRelationsAreValid(rels); err != nil {
354
return errgo.Mask(err, errgo.Is(params.ErrInvalidEntity))
357
if err := checkConsistentSeries(m.Series); err != nil {
358
return errgo.Mask(err, errgo.Is(params.ErrInvalidEntity))
363
func checkRelationsAreValid(rels map[string]charm.Relation) error {
364
for _, rel := range rels {
365
if rel.Name == "relation-name" {
366
return errgo.WithCausef(nil, params.ErrInvalidEntity, "relation %s has almost certainly not been changed from the template", rel.Name)
368
if rel.Interface == "interface-name" {
369
return errgo.WithCausef(nil, params.ErrInvalidEntity, "interface %s in relation %s has almost certainly not been changed from the template", rel.Interface, rel.Name)
375
// checkConsistentSeries ensures that all of the series listed in the
376
// charm metadata come from the same distribution. If an error is
377
// returned it will have a cause of params.ErrInvalidEntity.
378
func checkConsistentSeries(metadataSeries []string) error {
379
var dist series.Distribution
380
for _, s := range metadataSeries {
381
d := series.Series[s].Distribution
383
return errgo.WithCausef(nil, params.ErrInvalidEntity, "unrecognized series %q in metadata", s)
387
} else if dist != d {
388
return errgo.WithCausef(nil, params.ErrInvalidEntity, "cannot mix series from %s and %s in single charm", dist, d)
394
// checkIdAllowed ensures that the given id may be used for the provided
395
// charm. If an error is returned it will have a cause of
396
// params.ErrEntityIdNotAllowed.
397
func checkIdAllowed(id *router.ResolvedURL, ch charm.Charm) error {
399
if id.URL.Series == "" && len(m.Series) == 0 {
400
return errgo.WithCausef(nil, params.ErrEntityIdNotAllowed, "series not specified in url or charm metadata")
401
} else if id.URL.Series == "" || len(m.Series) == 0 {
404
// if we get here we have series in both the id and metadata, ensure they agree.
405
for _, s := range m.Series {
406
if s == id.URL.Series {
410
return errgo.WithCausef(nil, params.ErrEntityIdNotAllowed, "%q series not listed in charm metadata", id.URL.Series)
413
// addCharm adds a charm to the entities collection with the given parameters.
414
// If p.URL cannot be used as a name for the charm then the returned
415
// error will have the cause params.ErrEntityIdNotAllowed. If the charm
416
// duplicates an existing charm then the returned error will have the
417
// cause params.ErrDuplicateUpload.
418
func (s *Store) addCharm(c charm.Charm, p addParams) (err error) {
419
// Strictly speaking this test is redundant, because a ResolvedURL should
420
// always be canonical, but check just in case anyway, as this is
421
// final gateway before a potentially invalid url might be stored
424
logger.Infof("add charm url %s; promulgated rev %d", &id, p.url.PromulgatedRevision)
425
entity := &mongodoc.Entity{
427
PromulgatedURL: p.url.PromulgatedURL(),
428
BlobHash: p.blobHash,
429
BlobHash256: p.blobHash256,
430
BlobName: p.blobName,
431
PreV5BlobSize: p.preV5BlobSize,
432
PreV5BlobHash: p.preV5BlobHash,
433
PreV5BlobHash256: p.preV5BlobHash256,
435
UploadTime: time.Now(),
437
CharmConfig: c.Config(),
438
CharmActions: c.Actions(),
439
CharmProvidedInterfaces: interfacesForRelations(c.Meta().Provides),
440
CharmRequiredInterfaces: interfacesForRelations(c.Meta().Requires),
441
SupportedSeries: c.Meta().Series,
443
denormalizeEntity(entity)
444
setEntityChannels(entity, p.chans)
446
// Check that we're not going to create a charm that duplicates
447
// the name of a bundle. This is racy, but it's the best we can
448
// do. Also check that there isn't an existing multi-series charm
449
// that would be replaced by this one.
450
entities, err := s.FindEntities(entity.BaseURL, nil)
452
return errgo.Notef(err, "cannot check for existing entities")
454
for _, entity := range entities {
455
if entity.URL.Series == "bundle" {
456
return errgo.WithCausef(err, params.ErrEntityIdNotAllowed, "charm name duplicates bundle name %v", entity.URL)
458
if id.Series != "" && entity.URL.Series == "" {
459
return errgo.WithCausef(err, params.ErrEntityIdNotAllowed, "charm name duplicates multi-series charm name %v", entity.URL)
462
if err := s.addEntity(entity); err != nil {
463
return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload))
468
// setEntityChannels associates the entity with the given channels, ignoring
470
func setEntityChannels(entity *mongodoc.Entity, chans []params.Channel) {
471
for _, c := range chans {
473
case params.DevelopmentChannel:
474
entity.Development = true
475
case params.StableChannel:
481
// addBundle adds a bundle to the entities collection with the given
482
// parameters. If p.URL cannot be used as a name for the bundle then the
483
// returned error will have the cause params.ErrEntityIdNotAllowed. If
484
// the bundle duplicates an existing bundle then the returned error will
485
// have the cause params.ErrDuplicateUpload.
486
func (s *Store) addBundle(b charm.Bundle, p addParams) error {
487
bundleData := b.Data()
488
urls, err := bundleCharms(bundleData)
490
return errgo.Mask(err)
492
entity := &mongodoc.Entity{
494
BlobHash: p.blobHash,
495
BlobHash256: p.blobHash256,
496
BlobName: p.blobName,
497
PreV5BlobSize: p.preV5BlobSize,
498
PreV5BlobHash: p.preV5BlobHash,
499
PreV5BlobHash256: p.preV5BlobHash256,
501
UploadTime: time.Now(),
502
BundleData: bundleData,
503
BundleUnitCount: newInt(bundleUnitCount(bundleData)),
504
BundleMachineCount: newInt(bundleMachineCount(bundleData)),
505
BundleReadMe: b.ReadMe(),
507
PromulgatedURL: p.url.PromulgatedURL(),
509
denormalizeEntity(entity)
510
setEntityChannels(entity, p.chans)
512
// Check that we're not going to create a bundle that duplicates
513
// the name of a charm. This is racy, but it's the best we can do.
514
entities, err := s.FindEntities(entity.BaseURL, nil)
516
return errgo.Notef(err, "cannot check for existing entities")
518
for _, entity := range entities {
519
if entity.URL.Series != "bundle" {
520
return errgo.WithCausef(err, params.ErrEntityIdNotAllowed, "bundle name duplicates charm name %s", entity.URL)
523
if err := s.addEntity(entity); err != nil {
524
return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload))
529
// addEntity actually adds the entity (and its base entity if required) to
530
// the database. It assumes that the blob associated with the
531
// entity has already been validated and stored.
532
func (s *Store) addEntity(entity *mongodoc.Entity) (err error) {
533
// Add the base entity to the database.
534
perms := []string{entity.User}
535
acls := mongodoc.ACL{
539
baseEntity := &mongodoc.BaseEntity{
543
ChannelACLs: map[params.Channel]mongodoc.ACL{
544
params.UnpublishedChannel: acls,
545
params.DevelopmentChannel: acls,
546
params.StableChannel: acls,
548
Promulgated: entity.PromulgatedURL != nil,
550
err = s.DB.BaseEntities().Insert(baseEntity)
551
if err != nil && !mgo.IsDup(err) {
552
return errgo.Notef(err, "cannot insert base entity")
555
// Add the entity to the database.
556
err = s.DB.Entities().Insert(entity)
558
return params.ErrDuplicateUpload
561
return errgo.Notef(err, "cannot insert entity")
566
// denormalizeEntity sets all denormalized fields in e
567
// from their associated canonical fields.
569
// It is the responsibility of the caller to set e.SupportedSeries
570
// if the entity URL does not contain a series. If the entity
571
// URL *does* contain a series, e.SupportedSeries will
573
func denormalizeEntity(e *mongodoc.Entity) {
574
e.BaseURL = mongodoc.BaseURL(e.URL)
577
e.Revision = e.URL.Revision
578
e.Series = e.URL.Series
579
if e.URL.Series != "" {
580
if e.URL.Series == "bundle" {
581
e.SupportedSeries = nil
583
e.SupportedSeries = []string{e.URL.Series}
586
if e.PromulgatedURL == nil {
587
e.PromulgatedRevision = -1
589
e.PromulgatedRevision = e.PromulgatedURL.Revision
593
// newBundle returns a new bundle implementation from the archive blob
594
// read from r, that should have the given size and will
595
// be named with the given id.
597
// The bundle is checked for validity before returning.
598
func (s *Store) newBundle(id *router.ResolvedURL, r io.ReadSeeker, blobSize int64) (charm.Bundle, error) {
599
readerAt := ReaderAtSeeker(r)
600
b, err := charm.ReadBundleArchiveFromReader(readerAt, blobSize)
602
return nil, zipReadError(err, "cannot read bundle archive")
604
bundleData := b.Data()
605
charms, err := s.bundleCharms(bundleData.RequiredCharms())
607
return nil, errgo.Notef(err, "cannot retrieve bundle charms")
609
if err := bundleData.VerifyWithCharms(verifyConstraints, verifyStorage, charms); err != nil {
610
// TODO frankban: use multiError (defined in internal/router).
611
return nil, errgo.NoteMask(verificationError(err), "bundle verification failed", errgo.Is(params.ErrInvalidEntity))
616
func (s *Store) bundleCharms(ids []string) (map[string]charm.Charm, error) {
618
urls := make([]*charm.URL, 0, numIds)
619
idKeys := make([]string, 0, numIds)
620
// TODO resolve ids concurrently.
621
for _, id := range ids {
622
url, err := charm.ParseURL(id)
624
// Ignore this error. This will be caught in the bundle
625
// verification process (see bundleData.VerifyWithCharms) and will
626
// be returned to the user along with other bundle errors.
629
e, err := s.FindBestEntity(url, params.NoChannel, map[string]int{})
631
if errgo.Cause(err) == params.ErrNotFound {
632
// Ignore this error too, for the same reasons
638
urls = append(urls, e.URL)
639
idKeys = append(idKeys, id)
641
var entities []mongodoc.Entity
642
if err := s.DB.Entities().
643
Find(bson.D{{"_id", bson.D{{"$in", urls}}}}).
644
All(&entities); err != nil {
648
entityCharms := make(map[charm.URL]charm.Charm, len(entities))
649
for i, entity := range entities {
650
entityCharms[*entity.URL] = &entityCharm{entities[i]}
652
charms := make(map[string]charm.Charm, len(urls))
653
for i, url := range urls {
654
if ch, ok := entityCharms[*url]; ok {
655
charms[idKeys[i]] = ch
661
// bundleCharms returns all the charm URLs used by a bundle,
662
// without duplicates.
663
// TODO this seems to overlap slightly with Store.bundleCharms.
664
func bundleCharms(data *charm.BundleData) ([]*charm.URL, error) {
665
// Use a map to de-duplicate the URL list: a bundle can include services
666
// deployed by the same charm.
667
urlMap := make(map[string]*charm.URL)
668
for _, service := range data.Services {
669
url, err := charm.ParseURL(service.Charm)
671
return nil, errgo.Mask(err)
673
urlMap[url.String()] = url
674
// Also add the corresponding base URL.
675
base := mongodoc.BaseURL(url)
676
urlMap[base.String()] = base
678
urls := make([]*charm.URL, 0, len(urlMap))
679
for _, url := range urlMap {
680
urls = append(urls, url)
685
func newInt(x int) *int {
689
// bundleUnitCount returns the number of units created by the bundle.
690
func bundleUnitCount(b *charm.BundleData) int {
692
for _, service := range b.Services {
693
count += service.NumUnits
698
// bundleMachineCount returns the number of machines
699
// that will be created or used by the bundle.
700
func bundleMachineCount(b *charm.BundleData) int {
701
count := len(b.Machines)
702
for _, service := range b.Services {
703
// The default placement is "new".
704
placement := &charm.UnitPlacement{
707
// Check for "new" placements, which means a new machine
709
for _, location := range service.To {
711
placement, err = charm.ParsePlacement(location)
713
// Ignore invalid placements - a bundle should always
714
// be verified before adding to the charm store so this
715
// should never happen in practice.
718
if placement.Machine == "new" {
722
// If there are less elements in To than NumUnits, the last placement
723
// element is replicated. For this reason, if the last element is
724
// "new", we need to add more machines.
725
if placement != nil && placement.Machine == "new" {
726
count += service.NumUnits - len(service.To)
732
func interfacesForRelations(rels map[string]charm.Relation) []string {
733
// Eliminate duplicates by storing interface names into a map.
734
interfaces := make(map[string]bool)
735
for _, rel := range rels {
736
interfaces[rel.Interface] = true
738
result := make([]string, 0, len(interfaces))
739
for iface := range interfaces {
740
result = append(result, iface)
745
// zipReadError creates an appropriate error for errors in reading an
746
// uploaded archive. If the archive could not be read because the data
747
// uploaded is invalid then an error with a cause of
748
// params.ErrInvalidEntity will be returned. The given message will be
750
func zipReadError(err error, msg string) error {
751
switch errgo.Cause(err) {
752
case zip.ErrFormat, zip.ErrAlgorithm, zip.ErrChecksum:
753
return errgo.WithCausef(err, params.ErrInvalidEntity, msg)
755
return errgo.Notef(err, msg)
758
func verifyConstraints(s string) error {
759
// TODO(rog) provide some actual constraints checking here.
763
func verifyStorage(s string) error {
764
// TODO(frankban) provide some actual storage checking here.
768
// verificationError returns an error whose string representation is a list of
769
// all the verification error messages stored in err, in JSON format.
770
// Note that err must be a *charm.VerificationError.
771
func verificationError(err error) error {
772
verr, ok := err.(*charm.VerificationError)
776
messages := make([]string, len(verr.Errors))
777
for i, err := range verr.Errors {
778
messages[i] = err.Error()
780
sort.Strings(messages)
781
encodedMessages, err := json.Marshal(messages)
783
// This should never happen.
786
return errgo.WithCausef(nil, params.ErrInvalidEntity, string(encodedMessages))
789
// entityCharm implements charm.Charm.
790
type entityCharm struct {
794
func (e *entityCharm) Meta() *charm.Meta {
798
func (e *entityCharm) Metrics() *charm.Metrics {
802
func (e *entityCharm) Config() *charm.Config {
806
func (e *entityCharm) Actions() *charm.Actions {
807
return e.CharmActions
810
func (e *entityCharm) Revision() int {
811
return e.URL.Revision