~juju-qa/ubuntu/xenial/juju/xenial-2.0-beta3

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v5-unstable/internal/charmstore/addentity.go

  • Committer: Martin Packman
  • Date: 2016-03-30 19:31:08 UTC
  • mfrom: (1.1.41)
  • Revision ID: martin.packman@canonical.com-20160330193108-h9iz3ak334uk0z5r
Merge new upstream source 2.0~beta3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2016 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package charmstore // import "gopkg.in/juju/charmstore.v5-unstable/internal/charmstore"
 
5
 
 
6
import (
 
7
        "archive/zip"
 
8
        "bytes"
 
9
        "crypto/sha256"
 
10
        "crypto/sha512"
 
11
        "encoding/json"
 
12
        "fmt"
 
13
        "io"
 
14
        "io/ioutil"
 
15
        "sort"
 
16
        "time"
 
17
 
 
18
        jujuzip "github.com/juju/zip"
 
19
        "gopkg.in/errgo.v1"
 
20
        "gopkg.in/juju/charm.v6-unstable"
 
21
        "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
 
22
        "gopkg.in/mgo.v2"
 
23
        "gopkg.in/mgo.v2/bson"
 
24
        "gopkg.in/yaml.v2"
 
25
 
 
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"
 
30
)
 
31
 
 
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
 
37
        // be promulgated.
 
38
        url *router.ResolvedURL
 
39
 
 
40
        // blobName holds the name of the entity's archive blob.
 
41
        blobName string
 
42
 
 
43
        // blobHash holds the hash of the entity's archive blob.
 
44
        blobHash string
 
45
 
 
46
        // preV5BlobHash holds the hash of the entity's archive blob for
 
47
        // pre-v5 compatibility purposes.
 
48
        preV5BlobHash string
 
49
 
 
50
        // preV5BlobHash256 holds the SHA256 hash of the entity's archive blob for
 
51
        // pre-v5 compatibility purposes.
 
52
        preV5BlobHash256 string
 
53
 
 
54
        // preV5BlobSize holds the size of the entity's archive blob for
 
55
        // pre-v5 compatibility purposes.
 
56
        preV5BlobSize int64
 
57
 
 
58
        // blobHash256 holds the sha256 hash of the entity's archive blob.
 
59
        blobHash256 string
 
60
 
 
61
        // bobSize holds the size of the entity's archive blob.
 
62
        blobSize int64
 
63
 
 
64
        // chans holds the channels to associate with the entity.
 
65
        chans []params.Channel
 
66
}
 
67
 
 
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.
 
71
//
 
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)
 
75
}
 
76
 
 
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.
 
80
//
 
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)
 
84
}
 
85
 
 
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
 
90
// ArchiverTo.
 
91
func (s *Store) AddEntityWithArchive(url *router.ResolvedURL, archive interface{}) error {
 
92
        blob, err := getArchive(archive)
 
93
        if err != nil {
 
94
                return errgo.Notef(err, "cannot get archive")
 
95
        }
 
96
        defer blob.Close()
 
97
        hash := blobstore.NewHash()
 
98
        size, err := io.Copy(hash, blob)
 
99
        if err != nil {
 
100
                return errgo.Notef(err, "cannot copy archive")
 
101
        }
 
102
        if _, err := blob.Seek(0, 0); err != nil {
 
103
                return errgo.Notef(err, "cannot seek to start of archive")
 
104
        }
 
105
        if err := s.UploadEntity(url, blob, fmt.Sprintf("%x", hash.Sum(nil)), size, nil); err != nil {
 
106
                return errgo.Mask(err, errgo.Any)
 
107
        }
 
108
        return nil
 
109
}
 
110
 
 
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).
 
114
//
 
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
 
123
        // in the database.
 
124
        if url.URL.User == "" {
 
125
                return errgo.WithCausef(nil, params.ErrEntityIdNotAllowed, "entity id does not specify user")
 
126
        }
 
127
        if url.URL.Revision == -1 {
 
128
                return errgo.WithCausef(nil, params.ErrEntityIdNotAllowed, "entity id does not specify revision")
 
129
        }
 
130
        blobName, blobHash256, err := s.putArchive(blob, size, blobHash)
 
131
        if err != nil {
 
132
                return errgo.Mask(err)
 
133
        }
 
134
        r, _, err := s.BlobStore.Open(blobName)
 
135
        if err != nil {
 
136
                return errgo.Notef(err, "cannot open newly created blob")
 
137
        }
 
138
        defer r.Close()
 
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)
 
142
                }
 
143
                return errgo.Mask(err,
 
144
                        errgo.Is(params.ErrDuplicateUpload),
 
145
                        errgo.Is(params.ErrEntityIdNotAllowed),
 
146
                        errgo.Is(params.ErrInvalidEntity),
 
147
                )
 
148
        }
 
149
        return nil
 
150
}
 
151
 
 
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()
 
157
 
 
158
        // Calculate the SHA256 hash while uploading the blob in the blob store.
 
159
        hash256 := sha256.New()
 
160
        blob = io.TeeReader(blob, hash256)
 
161
 
 
162
        // Upload the actual blob, and make sure that it is removed
 
163
        // if we fail later.
 
164
        err = s.BlobStore.PutUnchallenged(blob, name, blobSize, hash)
 
165
        if err != nil {
 
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")
 
169
        }
 
170
        return name, fmt.Sprintf("%x", hash256.Sum(nil)), nil
 
171
}
 
172
 
 
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 {
 
176
        p := addParams{
 
177
                url:              id,
 
178
                blobName:         blobName,
 
179
                blobHash:         hash,
 
180
                blobHash256:      hash256,
 
181
                blobSize:         blobSize,
 
182
                preV5BlobHash:    hash,
 
183
                preV5BlobHash256: hash256,
 
184
                preV5BlobSize:    blobSize,
 
185
                chans:            chans,
 
186
        }
 
187
        if id.URL.Series == "bundle" {
 
188
                b, err := s.newBundle(id, r, blobSize)
 
189
                if err != nil {
 
190
                        return errgo.Mask(err, errgo.Is(params.ErrInvalidEntity), errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
 
191
                }
 
192
                if err := s.addBundle(b, p); err != nil {
 
193
                        return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
 
194
                }
 
195
                return nil
 
196
        }
 
197
        ch, err := s.newCharm(id, r, blobSize)
 
198
        if err != nil {
 
199
                return errgo.Mask(err, errgo.Is(params.ErrInvalidEntity), errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
 
200
        }
 
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")
 
204
                }
 
205
                logger.Infof("adding pre-v5 compat blob for %#v", id)
 
206
                info, err := addPreV5CompatibilityHackBlob(s.BlobStore, r, p.blobName, p.blobSize)
 
207
                if err != nil {
 
208
                        return errgo.Notef(err, "cannot add pre-v5 compatibility blob")
 
209
                }
 
210
                p.preV5BlobHash = info.hash
 
211
                p.preV5BlobHash256 = info.hash256
 
212
                p.preV5BlobSize = info.size
 
213
        }
 
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)
 
220
                }
 
221
        }
 
222
        if err != nil {
 
223
                return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload), errgo.Is(params.ErrEntityIdNotAllowed))
 
224
        }
 
225
        return nil
 
226
}
 
227
 
 
228
type preV5CompatibilityHackBlobInfo struct {
 
229
        hash    string
 
230
        hash256 string
 
231
        size    int64
 
232
}
 
233
 
 
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.
 
239
//
 
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)
 
247
        if err != nil {
 
248
                return nil, errgo.Notef(err, "cannot open charm archive")
 
249
        }
 
250
        var metadataf *jujuzip.File
 
251
        for _, f := range z.File {
 
252
                if f.Name == "metadata.yaml" {
 
253
                        metadataf = f
 
254
                        break
 
255
                }
 
256
        }
 
257
        if metadataf == nil {
 
258
                return nil, errgo.New("no metadata.yaml file found")
 
259
        }
 
260
        fr, err := metadataf.Open()
 
261
        if err != nil {
 
262
                return nil, errgo.Notef(err, "cannot open metadata.yaml from archive")
 
263
        }
 
264
        defer fr.Close()
 
265
        data, err := removeSeriesField(fr)
 
266
        if err != nil {
 
267
                return nil, errgo.Notef(err, "cannot remove series field from metadata")
 
268
        }
 
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)
 
273
        if err != nil {
 
274
                return nil, errgo.Notef(err, "cannot create appended metadata entry")
 
275
        }
 
276
        if _, err := zwf.Write(data); err != nil {
 
277
                return nil, errgo.Notef(err, "cannot write appended metadata data")
 
278
        }
 
279
        if err := zw.Close(); err != nil {
 
280
                return nil, errgo.Notef(err, "cannot close zip file")
 
281
        }
 
282
        data = appendedBlob.Bytes()
 
283
        sha384sum := sha512.Sum384(data)
 
284
 
 
285
        err = blobStore.PutUnchallenged(&appendedBlob, preV5CompatibilityBlobName(blobName), int64(len(data)), fmt.Sprintf("%x", sha384sum[:]))
 
286
        if err != nil {
 
287
                return nil, errgo.Notef(err, "cannot put archive blob")
 
288
        }
 
289
 
 
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")
 
295
        }
 
296
        if _, err := io.Copy(hashw, r); err != nil {
 
297
                return nil, errgo.Notef(err, "cannot recalculate blob checksum")
 
298
        }
 
299
        hashw.Write(data)
 
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)),
 
304
        }, nil
 
305
}
 
306
 
 
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"
 
311
}
 
312
 
 
313
func removeSeriesField(r io.Reader) ([]byte, error) {
 
314
        data, err := ioutil.ReadAll(r)
 
315
        if err != nil {
 
316
                return nil, errgo.Mask(err)
 
317
        }
 
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")
 
321
        }
 
322
        delete(meta, "series")
 
323
        data, err = yaml.Marshal(meta)
 
324
        if err != nil {
 
325
                return nil, errgo.Notef(err, "cannot re-marshal metadata.yaml")
 
326
        }
 
327
        return data, nil
 
328
}
 
329
 
 
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.
 
333
//
 
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)
 
338
        if err != nil {
 
339
                return nil, zipReadError(err, "cannot read charm archive")
 
340
        }
 
341
        if err := checkCharmIsValid(ch); err != nil {
 
342
                return nil, errgo.Mask(err, errgo.Is(params.ErrInvalidEntity))
 
343
        }
 
344
        if err := checkIdAllowed(id, ch); err != nil {
 
345
                return nil, errgo.Mask(err, errgo.Is(params.ErrEntityIdNotAllowed))
 
346
        }
 
347
        return ch, nil
 
348
}
 
349
 
 
350
func checkCharmIsValid(ch charm.Charm) error {
 
351
        m := ch.Meta()
 
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))
 
355
                }
 
356
        }
 
357
        if err := checkConsistentSeries(m.Series); err != nil {
 
358
                return errgo.Mask(err, errgo.Is(params.ErrInvalidEntity))
 
359
        }
 
360
        return nil
 
361
}
 
362
 
 
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)
 
367
                }
 
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)
 
370
                }
 
371
        }
 
372
        return nil
 
373
}
 
374
 
 
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
 
382
                if d == "" {
 
383
                        return errgo.WithCausef(nil, params.ErrInvalidEntity, "unrecognized series %q in metadata", s)
 
384
                }
 
385
                if dist == "" {
 
386
                        dist = d
 
387
                } else if dist != d {
 
388
                        return errgo.WithCausef(nil, params.ErrInvalidEntity, "cannot mix series from %s and %s in single charm", dist, d)
 
389
                }
 
390
        }
 
391
        return nil
 
392
}
 
393
 
 
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 {
 
398
        m := ch.Meta()
 
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 {
 
402
                return nil
 
403
        }
 
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 {
 
407
                        return nil
 
408
                }
 
409
        }
 
410
        return errgo.WithCausef(nil, params.ErrEntityIdNotAllowed, "%q series not listed in charm metadata", id.URL.Series)
 
411
}
 
412
 
 
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
 
422
        // in the database.
 
423
        id := p.url.URL
 
424
        logger.Infof("add charm url %s; promulgated rev %d", &id, p.url.PromulgatedRevision)
 
425
        entity := &mongodoc.Entity{
 
426
                URL:                     &id,
 
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,
 
434
                Size:                    p.blobSize,
 
435
                UploadTime:              time.Now(),
 
436
                CharmMeta:               c.Meta(),
 
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,
 
442
        }
 
443
        denormalizeEntity(entity)
 
444
        setEntityChannels(entity, p.chans)
 
445
 
 
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)
 
451
        if err != nil {
 
452
                return errgo.Notef(err, "cannot check for existing entities")
 
453
        }
 
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)
 
457
                }
 
458
                if id.Series != "" && entity.URL.Series == "" {
 
459
                        return errgo.WithCausef(err, params.ErrEntityIdNotAllowed, "charm name duplicates multi-series charm name %v", entity.URL)
 
460
                }
 
461
        }
 
462
        if err := s.addEntity(entity); err != nil {
 
463
                return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload))
 
464
        }
 
465
        return nil
 
466
}
 
467
 
 
468
// setEntityChannels associates the entity with the given channels, ignoring
 
469
// unknown channels.
 
470
func setEntityChannels(entity *mongodoc.Entity, chans []params.Channel) {
 
471
        for _, c := range chans {
 
472
                switch c {
 
473
                case params.DevelopmentChannel:
 
474
                        entity.Development = true
 
475
                case params.StableChannel:
 
476
                        entity.Stable = true
 
477
                }
 
478
        }
 
479
}
 
480
 
 
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)
 
489
        if err != nil {
 
490
                return errgo.Mask(err)
 
491
        }
 
492
        entity := &mongodoc.Entity{
 
493
                URL:                &p.url.URL,
 
494
                BlobHash:           p.blobHash,
 
495
                BlobHash256:        p.blobHash256,
 
496
                BlobName:           p.blobName,
 
497
                PreV5BlobSize:      p.preV5BlobSize,
 
498
                PreV5BlobHash:      p.preV5BlobHash,
 
499
                PreV5BlobHash256:   p.preV5BlobHash256,
 
500
                Size:               p.blobSize,
 
501
                UploadTime:         time.Now(),
 
502
                BundleData:         bundleData,
 
503
                BundleUnitCount:    newInt(bundleUnitCount(bundleData)),
 
504
                BundleMachineCount: newInt(bundleMachineCount(bundleData)),
 
505
                BundleReadMe:       b.ReadMe(),
 
506
                BundleCharms:       urls,
 
507
                PromulgatedURL:     p.url.PromulgatedURL(),
 
508
        }
 
509
        denormalizeEntity(entity)
 
510
        setEntityChannels(entity, p.chans)
 
511
 
 
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)
 
515
        if err != nil {
 
516
                return errgo.Notef(err, "cannot check for existing entities")
 
517
        }
 
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)
 
521
                }
 
522
        }
 
523
        if err := s.addEntity(entity); err != nil {
 
524
                return errgo.Mask(err, errgo.Is(params.ErrDuplicateUpload))
 
525
        }
 
526
        return nil
 
527
}
 
528
 
 
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{
 
536
                Read:  perms,
 
537
                Write: perms,
 
538
        }
 
539
        baseEntity := &mongodoc.BaseEntity{
 
540
                URL:  entity.BaseURL,
 
541
                User: entity.User,
 
542
                Name: entity.Name,
 
543
                ChannelACLs: map[params.Channel]mongodoc.ACL{
 
544
                        params.UnpublishedChannel: acls,
 
545
                        params.DevelopmentChannel: acls,
 
546
                        params.StableChannel:      acls,
 
547
                },
 
548
                Promulgated: entity.PromulgatedURL != nil,
 
549
        }
 
550
        err = s.DB.BaseEntities().Insert(baseEntity)
 
551
        if err != nil && !mgo.IsDup(err) {
 
552
                return errgo.Notef(err, "cannot insert base entity")
 
553
        }
 
554
 
 
555
        // Add the entity to the database.
 
556
        err = s.DB.Entities().Insert(entity)
 
557
        if mgo.IsDup(err) {
 
558
                return params.ErrDuplicateUpload
 
559
        }
 
560
        if err != nil {
 
561
                return errgo.Notef(err, "cannot insert entity")
 
562
        }
 
563
        return nil
 
564
}
 
565
 
 
566
// denormalizeEntity sets all denormalized fields in e
 
567
// from their associated canonical fields.
 
568
//
 
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
 
572
// be overwritten.
 
573
func denormalizeEntity(e *mongodoc.Entity) {
 
574
        e.BaseURL = mongodoc.BaseURL(e.URL)
 
575
        e.Name = e.URL.Name
 
576
        e.User = e.URL.User
 
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
 
582
                } else {
 
583
                        e.SupportedSeries = []string{e.URL.Series}
 
584
                }
 
585
        }
 
586
        if e.PromulgatedURL == nil {
 
587
                e.PromulgatedRevision = -1
 
588
        } else {
 
589
                e.PromulgatedRevision = e.PromulgatedURL.Revision
 
590
        }
 
591
}
 
592
 
 
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.
 
596
//
 
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)
 
601
        if err != nil {
 
602
                return nil, zipReadError(err, "cannot read bundle archive")
 
603
        }
 
604
        bundleData := b.Data()
 
605
        charms, err := s.bundleCharms(bundleData.RequiredCharms())
 
606
        if err != nil {
 
607
                return nil, errgo.Notef(err, "cannot retrieve bundle charms")
 
608
        }
 
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))
 
612
        }
 
613
        return b, nil
 
614
}
 
615
 
 
616
func (s *Store) bundleCharms(ids []string) (map[string]charm.Charm, error) {
 
617
        numIds := len(ids)
 
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)
 
623
                if err != nil {
 
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.
 
627
                        continue
 
628
                }
 
629
                e, err := s.FindBestEntity(url, params.NoChannel, map[string]int{})
 
630
                if err != nil {
 
631
                        if errgo.Cause(err) == params.ErrNotFound {
 
632
                                // Ignore this error too, for the same reasons
 
633
                                // described above.
 
634
                                continue
 
635
                        }
 
636
                        return nil, err
 
637
                }
 
638
                urls = append(urls, e.URL)
 
639
                idKeys = append(idKeys, id)
 
640
        }
 
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 {
 
645
                return nil, err
 
646
        }
 
647
 
 
648
        entityCharms := make(map[charm.URL]charm.Charm, len(entities))
 
649
        for i, entity := range entities {
 
650
                entityCharms[*entity.URL] = &entityCharm{entities[i]}
 
651
        }
 
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
 
656
                }
 
657
        }
 
658
        return charms, nil
 
659
}
 
660
 
 
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)
 
670
                if err != nil {
 
671
                        return nil, errgo.Mask(err)
 
672
                }
 
673
                urlMap[url.String()] = url
 
674
                // Also add the corresponding base URL.
 
675
                base := mongodoc.BaseURL(url)
 
676
                urlMap[base.String()] = base
 
677
        }
 
678
        urls := make([]*charm.URL, 0, len(urlMap))
 
679
        for _, url := range urlMap {
 
680
                urls = append(urls, url)
 
681
        }
 
682
        return urls, nil
 
683
}
 
684
 
 
685
func newInt(x int) *int {
 
686
        return &x
 
687
}
 
688
 
 
689
// bundleUnitCount returns the number of units created by the bundle.
 
690
func bundleUnitCount(b *charm.BundleData) int {
 
691
        count := 0
 
692
        for _, service := range b.Services {
 
693
                count += service.NumUnits
 
694
        }
 
695
        return count
 
696
}
 
697
 
 
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{
 
705
                        Machine: "new",
 
706
                }
 
707
                // Check for "new" placements, which means a new machine
 
708
                // must be added.
 
709
                for _, location := range service.To {
 
710
                        var err error
 
711
                        placement, err = charm.ParsePlacement(location)
 
712
                        if err != nil {
 
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.
 
716
                                continue
 
717
                        }
 
718
                        if placement.Machine == "new" {
 
719
                                count++
 
720
                        }
 
721
                }
 
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)
 
727
                }
 
728
        }
 
729
        return count
 
730
}
 
731
 
 
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
 
737
        }
 
738
        result := make([]string, 0, len(interfaces))
 
739
        for iface := range interfaces {
 
740
                result = append(result, iface)
 
741
        }
 
742
        return result
 
743
}
 
744
 
 
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
 
749
// added as context.
 
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)
 
754
        }
 
755
        return errgo.Notef(err, msg)
 
756
}
 
757
 
 
758
func verifyConstraints(s string) error {
 
759
        // TODO(rog) provide some actual constraints checking here.
 
760
        return nil
 
761
}
 
762
 
 
763
func verifyStorage(s string) error {
 
764
        // TODO(frankban) provide some actual storage checking here.
 
765
        return nil
 
766
}
 
767
 
 
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)
 
773
        if !ok {
 
774
                return err
 
775
        }
 
776
        messages := make([]string, len(verr.Errors))
 
777
        for i, err := range verr.Errors {
 
778
                messages[i] = err.Error()
 
779
        }
 
780
        sort.Strings(messages)
 
781
        encodedMessages, err := json.Marshal(messages)
 
782
        if err != nil {
 
783
                // This should never happen.
 
784
                return err
 
785
        }
 
786
        return errgo.WithCausef(nil, params.ErrInvalidEntity, string(encodedMessages))
 
787
}
 
788
 
 
789
// entityCharm implements charm.Charm.
 
790
type entityCharm struct {
 
791
        mongodoc.Entity
 
792
}
 
793
 
 
794
func (e *entityCharm) Meta() *charm.Meta {
 
795
        return e.CharmMeta
 
796
}
 
797
 
 
798
func (e *entityCharm) Metrics() *charm.Metrics {
 
799
        return nil
 
800
}
 
801
 
 
802
func (e *entityCharm) Config() *charm.Config {
 
803
        return e.CharmConfig
 
804
}
 
805
 
 
806
func (e *entityCharm) Actions() *charm.Actions {
 
807
        return e.CharmActions
 
808
}
 
809
 
 
810
func (e *entityCharm) Revision() int {
 
811
        return e.URL.Revision
 
812
}