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"
16
jc "github.com/juju/testing/checkers"
17
gc "gopkg.in/check.v1"
19
"gopkg.in/juju/charm.v6-unstable"
20
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
22
"gopkg.in/juju/charmstore.v5-unstable/internal/blobstore"
23
"gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
24
"gopkg.in/juju/charmstore.v5-unstable/internal/router"
25
"gopkg.in/juju/charmstore.v5-unstable/internal/storetesting"
28
type AddEntitySuite struct {
32
var _ = gc.Suite(&AddEntitySuite{})
34
func (s *AddEntitySuite) TestAddCharmWithUser(c *gc.C) {
35
store := s.newStore(c, false)
38
wordpress := storetesting.Charms.CharmDir("wordpress")
39
url := router.MustNewResolvedURL("cs:~who/precise/wordpress-23", -1)
40
err := store.AddCharmWithArchive(url, wordpress)
41
c.Assert(err, gc.IsNil)
42
assertBaseEntity(c, store, mongodoc.BaseURL(&url.URL), false)
45
func (s *AddEntitySuite) TestAddPromulgatedCharmDir(c *gc.C) {
46
charmDir := storetesting.Charms.CharmDir("wordpress")
47
s.checkAddCharm(c, charmDir, router.MustNewResolvedURL("~charmers/precise/wordpress-1", 1))
50
func (s *AddEntitySuite) TestAddPromulgatedCharmArchive(c *gc.C) {
51
charmArchive := storetesting.Charms.CharmArchive(c.MkDir(), "wordpress")
52
s.checkAddCharm(c, charmArchive, router.MustNewResolvedURL("~charmers/precise/wordpress-1", 1))
55
func (s *AddEntitySuite) TestAddUserOwnedCharmDir(c *gc.C) {
56
charmDir := storetesting.Charms.CharmDir("wordpress")
57
s.checkAddCharm(c, charmDir, router.MustNewResolvedURL("~charmers/precise/wordpress-1", -1))
60
func (s *AddEntitySuite) TestAddUserOwnedCharmArchive(c *gc.C) {
61
charmArchive := storetesting.Charms.CharmArchive(c.MkDir(), "wordpress")
62
s.checkAddCharm(c, charmArchive, router.MustNewResolvedURL("~charmers/precise/wordpress-1", -1))
65
func (s *AddEntitySuite) TestAddBundleDir(c *gc.C) {
66
bundleDir := storetesting.Charms.BundleDir("wordpress-simple")
67
s.checkAddBundle(c, bundleDir, router.MustNewResolvedURL("~charmers/bundle/wordpress-simple-2", 3))
70
func (s *AddEntitySuite) TestAddBundleArchive(c *gc.C) {
71
bundleArchive, err := charm.ReadBundleArchive(
72
storetesting.Charms.BundleArchivePath(c.MkDir(), "wordpress-simple"),
74
s.addRequiredCharms(c, bundleArchive)
75
c.Assert(err, gc.IsNil)
76
s.checkAddBundle(c, bundleArchive, router.MustNewResolvedURL("~charmers/bundle/wordpress-simple-2", 3))
79
func (s *AddEntitySuite) TestAddUserOwnedBundleDir(c *gc.C) {
80
bundleDir := storetesting.Charms.BundleDir("wordpress-simple")
81
s.checkAddBundle(c, bundleDir, router.MustNewResolvedURL("~charmers/bundle/wordpress-simple-1", -1))
84
func (s *AddEntitySuite) TestAddUserOwnedBundleArchive(c *gc.C) {
85
bundleArchive, err := charm.ReadBundleArchive(
86
storetesting.Charms.BundleArchivePath(c.MkDir(), "wordpress-simple"),
88
c.Assert(err, gc.IsNil)
89
s.checkAddBundle(c, bundleArchive, router.MustNewResolvedURL("~charmers/bundle/wordpress-simple-1", -1))
92
func (s *AddEntitySuite) TestAddCharmWithBundleSeries(c *gc.C) {
93
store := s.newStore(c, false)
95
ch := storetesting.Charms.CharmArchive(c.MkDir(), "wordpress")
96
err := store.AddCharmWithArchive(router.MustNewResolvedURL("~charmers/bundle/wordpress-2", -1), ch)
97
c.Assert(err, gc.ErrorMatches, `cannot read bundle archive: archive file "bundle.yaml" not found`)
100
func (s *AddEntitySuite) TestAddCharmWithMultiSeries(c *gc.C) {
101
store := s.newStore(c, false)
103
ch := storetesting.Charms.CharmArchive(c.MkDir(), "multi-series")
104
s.checkAddCharm(c, ch, router.MustNewResolvedURL("~charmers/multi-series-1", 1))
105
// Make sure it can be accessed with a number of names
106
e, err := store.FindBestEntity(charm.MustParseURL("~charmers/multi-series-1"), params.UnpublishedChannel, nil)
107
c.Assert(err, gc.IsNil)
108
c.Assert(e.URL.String(), gc.Equals, "cs:~charmers/multi-series-1")
109
e, err = store.FindBestEntity(charm.MustParseURL("~charmers/trusty/multi-series-1"), params.UnpublishedChannel, nil)
110
c.Assert(err, gc.IsNil)
111
c.Assert(e.URL.String(), gc.Equals, "cs:~charmers/multi-series-1")
112
e, err = store.FindBestEntity(charm.MustParseURL("~charmers/wily/multi-series-1"), params.UnpublishedChannel, nil)
113
c.Assert(err, gc.IsNil)
114
c.Assert(e.URL.String(), gc.Equals, "cs:~charmers/multi-series-1")
115
_, err = store.FindBestEntity(charm.MustParseURL("~charmers/precise/multi-series-1"), params.UnpublishedChannel, nil)
116
c.Assert(err, gc.ErrorMatches, "no matching charm or bundle for cs:~charmers/precise/multi-series-1")
117
c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound)
120
func (s *AddEntitySuite) TestAddCharmWithSeriesWhenThereIsAnExistingMultiSeriesVersion(c *gc.C) {
121
store := s.newStore(c, false)
123
ch := storetesting.Charms.CharmArchive(c.MkDir(), "multi-series")
124
err := store.AddCharmWithArchive(router.MustNewResolvedURL("~charmers/multi-series-1", -1), ch)
125
c.Assert(err, gc.IsNil)
126
ch = storetesting.Charms.CharmArchive(c.MkDir(), "wordpress")
127
err = store.AddCharmWithArchive(router.MustNewResolvedURL("~charmers/trusty/multi-series-2", -1), ch)
128
c.Assert(err, gc.ErrorMatches, `charm name duplicates multi-series charm name cs:~charmers/multi-series-1`)
131
func (s *AddEntitySuite) TestAddCharmWithMultiSeriesToES(c *gc.C) {
132
store := s.newStore(c, true)
134
ch := storetesting.Charms.CharmArchive(c.MkDir(), "multi-series")
135
s.checkAddCharm(c, ch, router.MustNewResolvedURL("~charmers/juju-gui-1", 1))
138
func (s *AddEntitySuite) TestAddBundleDuplicatingCharm(c *gc.C) {
139
store := s.newStore(c, false)
141
ch := storetesting.Charms.CharmDir("wordpress")
142
err := store.AddCharmWithArchive(router.MustNewResolvedURL("~tester/precise/wordpress-2", -1), ch)
143
c.Assert(err, gc.IsNil)
145
b := storetesting.Charms.BundleDir("wordpress-simple")
146
s.addRequiredCharms(c, b)
147
err = store.AddBundleWithArchive(router.MustNewResolvedURL("~tester/bundle/wordpress-5", -1), b)
148
c.Assert(err, gc.ErrorMatches, "bundle name duplicates charm name cs:~tester/precise/wordpress-2")
151
func (s *AddEntitySuite) TestAddCharmDuplicatingBundle(c *gc.C) {
152
store := s.newStore(c, false)
155
b := storetesting.Charms.BundleDir("wordpress-simple")
156
s.addRequiredCharms(c, b)
157
err := store.AddBundleWithArchive(router.MustNewResolvedURL("~charmers/bundle/wordpress-simple-2", -1), b)
158
c.Assert(err, gc.IsNil)
160
ch := storetesting.Charms.CharmDir("wordpress")
161
err = store.AddCharmWithArchive(router.MustNewResolvedURL("~charmers/precise/wordpress-simple-5", -1), ch)
162
c.Assert(err, gc.ErrorMatches, "charm name duplicates bundle name cs:~charmers/bundle/wordpress-simple-2")
165
var uploadEntityErrorsTests = []struct {
174
about: "revision not specified",
175
url: "~charmers/precise/wordpress",
176
upload: storetesting.NewCharm(nil),
177
expectError: "entity id does not specify revision",
178
expectCause: params.ErrEntityIdNotAllowed,
180
about: "user not specified",
181
url: "precise/wordpress-23",
182
upload: storetesting.NewCharm(nil),
183
expectError: "entity id does not specify user",
184
expectCause: params.ErrEntityIdNotAllowed,
186
about: "hash mismatch",
187
url: "~charmers/precise/wordpress-0",
188
upload: storetesting.NewCharm(nil),
189
blobHash: "blahblah",
190
expectError: "cannot put archive blob: hash mismatch",
191
// It would be nice if this was:
192
// expectCause: params.ErrInvalidEntity,
194
about: "size mismatch",
195
url: "~charmers/precise/wordpress-0",
196
upload: storetesting.NewCharm(nil),
198
expectError: "cannot read charm archive: seek past end of file",
199
// It would be nice if the above error was better and
201
// expectCause: params.ErrInvalidEntity,
203
about: "charm uploaded to bundle URL",
204
url: "~charmers/bundle/foo-0",
205
upload: storetesting.NewCharm(nil),
206
expectError: `cannot read bundle archive: archive file "bundle.yaml" not found`,
207
// It would be nice if this was:
208
// expectCause: params.ErrInvalidEntity,
210
about: "bundle uploaded to charm URL",
211
url: "~charmers/precise/foo-0",
212
upload: storetesting.NewBundle(&charm.BundleData{
213
Services: map[string]*charm.ServiceSpec{
219
expectError: `cannot read charm archive: archive file "metadata.yaml" not found`,
220
// It would be nice if this was:
221
// expectCause: params.ErrInvalidEntity,
223
about: "banned relation name",
224
url: "~charmers/precise/foo-0",
225
upload: storetesting.NewCharm(storetesting.RelationMeta("requires relation-name foo")),
226
expectError: `relation relation-name has almost certainly not been changed from the template`,
227
expectCause: params.ErrInvalidEntity,
229
about: "banned interface name",
230
url: "~charmers/precise/foo-0",
231
upload: storetesting.NewCharm(storetesting.RelationMeta("requires foo interface-name")),
232
expectError: `interface interface-name in relation foo has almost certainly not been changed from the template`,
233
expectCause: params.ErrInvalidEntity,
235
about: "unrecognized series",
236
url: "~charmers/precise/foo-0",
237
upload: storetesting.NewCharm(storetesting.MetaWithSupportedSeries(nil, "badseries")),
238
expectError: `unrecognized series "badseries" in metadata`,
239
expectCause: params.ErrInvalidEntity,
241
about: "inconsistent series",
242
url: "~charmers/trusty/foo-0",
243
upload: storetesting.NewCharm(storetesting.MetaWithSupportedSeries(nil, "trusty", "win10")),
244
expectError: `cannot mix series from ubuntu and windows in single charm`,
245
expectCause: params.ErrInvalidEntity,
247
about: "series not specified",
248
url: "~charmers/foo-0",
249
upload: storetesting.NewCharm(nil),
250
expectError: `series not specified in url or charm metadata`,
251
expectCause: params.ErrEntityIdNotAllowed,
253
about: "series not allowed by metadata",
254
url: "~charmers/precise/foo-0",
255
upload: storetesting.NewCharm(storetesting.MetaWithSupportedSeries(nil, "trusty")),
256
expectError: `"precise" series not listed in charm metadata`,
257
expectCause: params.ErrEntityIdNotAllowed,
259
about: "bundle refers to non-existent charm",
260
url: "~charmers/bundle/foo-0",
261
upload: storetesting.NewBundle(&charm.BundleData{
262
Services: map[string]*charm.ServiceSpec{
268
expectError: regexp.QuoteMeta(`bundle verification failed: ["service \"foo\" refers to non-existent charm \"bad-charm\""]`),
269
expectCause: params.ErrInvalidEntity,
271
about: "bundle verification fails",
272
url: "~charmers/bundle/foo-0",
273
upload: storetesting.NewBundle(&charm.BundleData{}),
274
expectError: regexp.QuoteMeta(`bundle verification failed: ["at least one service must be specified"]`),
275
expectCause: params.ErrInvalidEntity,
277
about: "invalid zip format",
278
url: "~charmers/foo-0",
279
upload: zipWithInvalidFormat(),
280
expectError: `cannot read charm archive: zip: not a valid zip file`,
281
expectCause: params.ErrInvalidEntity,
283
about: "invalid zip algorithm",
284
url: "~charmers/foo-0",
285
upload: zipWithInvalidAlgorithm(),
286
expectError: `cannot read charm archive: zip: unsupported compression algorithm`,
287
expectCause: params.ErrInvalidEntity,
289
about: "invalid zip checksum",
290
url: "~charmers/foo-0",
291
upload: zipWithInvalidChecksum(),
292
expectError: `cannot read charm archive: zip: checksum error`,
293
expectCause: params.ErrInvalidEntity,
296
func (s *AddEntitySuite) TestUploadEntityErrors(c *gc.C) {
297
store := s.newStore(c, true)
299
for i, test := range uploadEntityErrorsTests {
300
c.Logf("test %d: %s", i, test.about)
302
err := test.upload.ArchiveTo(&buf)
303
c.Assert(err, gc.IsNil)
304
if test.blobHash == "" {
305
h := blobstore.NewHash()
307
test.blobHash = fmt.Sprintf("%x", h.Sum(nil))
309
if test.blobSize == 0 {
310
test.blobSize = int64(len(buf.Bytes()))
312
url := &router.ResolvedURL{
313
URL: *charm.MustParseURL(test.url),
315
err = store.UploadEntity(url, &buf, test.blobHash, test.blobSize, nil)
316
c.Assert(err, gc.ErrorMatches, test.expectError)
317
if test.expectCause != nil {
318
c.Assert(errgo.Cause(err), gc.Equals, test.expectCause)
320
c.Assert(errgo.Cause(err), gc.Equals, err)
325
func (s *AddEntitySuite) checkAddCharm(c *gc.C, ch charm.Charm, url *router.ResolvedURL) {
326
store := s.newStore(c, true)
329
// Add the charm to the store.
330
beforeAdding := time.Now()
331
err := store.AddCharmWithArchive(url, ch)
332
c.Assert(err, gc.IsNil)
333
afterAdding := time.Now()
335
var doc *mongodoc.Entity
336
err = store.DB.Entities().FindId(&url.URL).One(&doc)
337
c.Assert(err, gc.IsNil)
339
// The entity doc has been correctly added to the mongo collection.
340
size, hash, hash256 := getSizeAndHashes(ch)
341
sort.Strings(doc.CharmProvidedInterfaces)
342
sort.Strings(doc.CharmRequiredInterfaces)
344
// Check the upload time and then reset it to its zero value
345
// so that we can test the deterministic parts later.
346
c.Assert(doc.UploadTime, jc.TimeBetween(beforeAdding, afterAdding))
348
doc.UploadTime = time.Time{}
350
assertDoc := assertBlobFields(c, doc, url, hash, hash256, size)
351
c.Assert(assertDoc, jc.DeepEquals, denormalizedEntity(&mongodoc.Entity{
354
BlobHash256: hash256,
356
CharmMeta: ch.Meta(),
357
CharmActions: ch.Actions(),
358
CharmConfig: ch.Config(),
359
CharmProvidedInterfaces: []string{"http", "logging", "monitoring"},
360
CharmRequiredInterfaces: []string{"mysql", "varnish"},
361
PromulgatedURL: url.PromulgatedURL(),
362
SupportedSeries: ch.Meta().Series,
365
// The charm archive has been properly added to the blob store.
366
r, obtainedSize, err := store.BlobStore.Open(doc.BlobName)
367
c.Assert(err, gc.IsNil)
369
c.Assert(obtainedSize, gc.Equals, size)
370
data, err := ioutil.ReadAll(r)
371
c.Assert(err, gc.IsNil)
372
charmArchive, err := charm.ReadCharmArchiveBytes(data)
373
c.Assert(err, gc.IsNil)
374
c.Assert(charmArchive.Meta(), jc.DeepEquals, ch.Meta())
375
c.Assert(charmArchive.Config(), jc.DeepEquals, ch.Config())
376
c.Assert(charmArchive.Actions(), jc.DeepEquals, ch.Actions())
377
c.Assert(charmArchive.Revision(), jc.DeepEquals, ch.Revision())
379
// Check that the base entity has been properly created.
380
assertBaseEntity(c, store, mongodoc.BaseURL(&url.URL), url.PromulgatedRevision != -1)
382
// Try inserting the charm again - it should fail because the charm is
384
err = store.AddCharmWithArchive(url, ch)
385
c.Assert(errgo.Cause(err), gc.Equals, params.ErrDuplicateUpload)
388
func (s *AddEntitySuite) checkAddBundle(c *gc.C, bundle charm.Bundle, url *router.ResolvedURL) {
389
store := s.newStore(c, true)
391
// Add the bundle to the store.
392
beforeAdding := time.Now()
393
s.addRequiredCharms(c, bundle)
394
err := store.AddBundleWithArchive(url, bundle)
395
c.Assert(err, gc.IsNil)
396
afterAdding := time.Now()
398
var doc *mongodoc.Entity
399
err = store.DB.Entities().FindId(&url.URL).One(&doc)
400
c.Assert(err, gc.IsNil)
401
sort.Sort(orderedURLs(doc.BundleCharms))
403
// Check the upload time and then reset it to its zero value
404
// so that we can test the deterministic parts later.
405
c.Assert(doc.UploadTime, jc.TimeBetween(beforeAdding, afterAdding))
406
doc.UploadTime = time.Time{}
408
// The entity doc has been correctly added to the mongo collection.
409
size, hash, hash256 := getSizeAndHashes(bundle)
411
assertDoc := assertBlobFields(c, doc, url, hash, hash256, size)
412
c.Assert(assertDoc, jc.DeepEquals, denormalizedEntity(&mongodoc.Entity{
415
BlobHash256: hash256,
417
BundleData: bundle.Data(),
418
BundleReadMe: bundle.ReadMe(),
419
BundleCharms: []*charm.URL{
420
charm.MustParseURL("mysql"),
421
charm.MustParseURL("wordpress"),
423
BundleMachineCount: newInt(2),
424
BundleUnitCount: newInt(2),
425
PromulgatedURL: url.PromulgatedURL(),
428
// The bundle archive has been properly added to the blob store.
429
r, obtainedSize, err := store.BlobStore.Open(doc.BlobName)
430
c.Assert(err, gc.IsNil)
432
c.Assert(obtainedSize, gc.Equals, size)
433
data, err := ioutil.ReadAll(r)
434
c.Assert(err, gc.IsNil)
435
bundleArchive, err := charm.ReadBundleArchiveBytes(data)
436
c.Assert(err, gc.IsNil)
437
c.Assert(bundleArchive.Data(), jc.DeepEquals, bundle.Data())
438
c.Assert(bundleArchive.ReadMe(), jc.DeepEquals, bundle.ReadMe())
440
// Check that the base entity has been properly created.
441
assertBaseEntity(c, store, mongodoc.BaseURL(&url.URL), url.PromulgatedRevision != -1)
443
// Try inserting the bundle again - it should fail because the bundle is
445
err = store.AddBundleWithArchive(url, bundle)
446
c.Assert(errgo.Cause(err), gc.Equals, params.ErrDuplicateUpload, gc.Commentf("error: %v", err))
449
// assertBlobFields asserts that the blob-related fields in doc are as expected.
450
// It returns a copy of doc with unpredictable fields zeroed out.
451
func assertBlobFields(c *gc.C, doc *mongodoc.Entity, url *router.ResolvedURL, hash, hash256 string, size int64) *mongodoc.Entity {
455
// The blob name is random, but we check that it's
456
// in the correct format, and non-empty.
457
blobName := doc.BlobName
458
c.Assert(blobName, gc.Matches, "[0-9a-z]+")
460
// The PreV5* fields are unpredictable, so zero them out
461
// for the purposes of comparison.
462
if doc.CharmMeta != nil && len(doc.CharmMeta.Series) > 0 {
463
// It's a multi-series charm, so the PreV5* fields should be active.
464
if doc.PreV5BlobSize <= doc.Size {
465
c.Fatalf("pre-v5 blobsize %d is unexpectedly less than original blob size %d", doc.PreV5BlobSize, doc.Size)
467
c.Assert(doc.PreV5BlobHash, gc.Not(gc.Equals), "")
468
c.Assert(doc.PreV5BlobHash, gc.Not(gc.Equals), hash)
469
c.Assert(doc.PreV5BlobHash256, gc.Not(gc.Equals), "")
470
c.Assert(doc.PreV5BlobHash256, gc.Not(gc.Equals), hash256)
472
c.Assert(doc.PreV5BlobSize, gc.Equals, doc.Size)
473
c.Assert(doc.PreV5BlobHash, gc.Equals, doc.BlobHash)
474
c.Assert(doc.PreV5BlobHash256, gc.Equals, doc.BlobHash256)
476
doc.PreV5BlobSize = 0
477
doc.PreV5BlobHash = ""
478
doc.PreV5BlobHash256 = ""
482
func assertBaseEntity(c *gc.C, store *Store, url *charm.URL, promulgated bool) {
483
baseEntity, err := store.FindBaseEntity(url, nil)
484
c.Assert(err, gc.IsNil)
485
acls := mongodoc.ACL{
486
Read: []string{url.User},
487
Write: []string{url.User},
489
expectACLs := map[params.Channel]mongodoc.ACL{
490
params.StableChannel: acls,
491
params.DevelopmentChannel: acls,
492
params.UnpublishedChannel: acls,
494
c.Assert(storetesting.NormalizeBaseEntity(baseEntity), jc.DeepEquals, storetesting.NormalizeBaseEntity(&mongodoc.BaseEntity{
498
Promulgated: mongodoc.IntBool(promulgated),
499
ChannelACLs: expectACLs,
503
type orderedURLs []*charm.URL
505
func (o orderedURLs) Less(i, j int) bool {
506
return o[i].String() < o[j].String()
509
func (o orderedURLs) Swap(i, j int) {
510
o[i], o[j] = o[j], o[i]
513
func (o orderedURLs) Len() int {
517
type byteArchiver []byte
519
func (a byteArchiver) ArchiveTo(w io.Writer) error {
524
func zipWithInvalidFormat() ArchiverTo {
525
return byteArchiver(nil)
528
func zipWithInvalidChecksum() ArchiverTo {
530
"PK\x03\x04\x14\x00\b\x00\x00\x00\x00\x00\x00" +
531
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +
532
"\x00\x00\r\x00\x00\x00metadata.yamlielloPK\a\b" +
533
"\x86\xa6\x106\x05\x00\x00\x00\x05\x00\x00\x00PK" +
534
"\x01\x02\x14\x00\x14\x00\b\x00\x00\x00\x00\x00" +
535
"\x00\x00\x86\xa6\x106\x05\x00\x00\x00\x05\x00" +
536
"\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x00\x00" +
537
"\x00\x00\x00\x00\x00\x00\x00\x00metadata.yamlPK" +
538
"\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00;\x00" +
539
"\x00\x00@\x00\x00\x00\x00\x00",
542
data := storetesting.NewCharm(nil).Bytes()
543
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
547
// Change the contents of a file so
548
// that it won't (probably) fit the checksum
550
off, err := zr.File[0].DataOffset()
555
return byteArchiver(data)
558
func zipWithInvalidAlgorithm() ArchiverTo {
560
"PK\x03\x04\x14\x00\b\x00\t\x00\x00\x00" +
561
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +
562
"\x00\x00\x00\x00\r\x00\x00\x00metadata.yamlhello" +
563
"PK\a\b\x86\xa6\x106\x05\x00\x00\x00\x05\x00" +
564
"\x00\x00PK\x01\x02\x14\x00\x14\x00\b\x00" +
565
"\t\x00\x00\x00\x00\x00\x86\xa6\x106\x05\x00" +
566
"\x00\x00\x05\x00\x00\x00\r\x00\x00\x00\x00" +
567
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +
568
"\x00\x00metadata.yamlPK\x05\x06\x00\x00\x00" +
569
"\x00\x01\x00\x01\x00;\x00\x00\x00@\x00\x00" +