forked from juju/juju
-
Notifications
You must be signed in to change notification settings - Fork 0
/
fakeclient.go
589 lines (529 loc) · 21.9 KB
/
fakeclient.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
// Copyright 2019 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
// fakeclient provides two charmstore client sthat do not use any HTTP connections
// or authentication mechanisms. Why two? Each reflects one of two informally-defined
// styles for interacting with the charmstore.
//
// - FakeClient has methods that include a channel parameter
// - ChannelAwareFakeClient maintains a record of the channel that it is currently talking about
//
// fakeclient also includes a Repository type. The Repository preforms the role of a substitute charmrepo.
// More technically, it implements the gopkg.in/juju/charmrepo Interface.
//
// Each of these three types are interrelated:
//
// Repository --> FakeClient --> ChannelAwareFakeClient
// | [extended by] | [extended by] |
// \ \ \
// provides storage provides front-end provides alternative
// for charms, front end
// bundles,
// resources
package charmstore
import (
"bytes"
"crypto/sha512"
"fmt"
"io"
"github.com/juju/errors"
"github.com/juju/utils/set"
"gopkg.in/juju/charm.v6"
"gopkg.in/juju/charmrepo.v3/csclient"
"gopkg.in/juju/charmrepo.v3/csclient/params"
"gopkg.in/macaroon.v2-unstable"
"github.com/juju/juju/api/charms"
)
// datastore is a small, in-memory key/value store. Its primary use case is to
// fake HTTP calls.
//
// Note
//
// datastore's methods support a path argument that represents a URL path. However,
// no normalisation is performed on this parameter. Therefore,
// two strings that refer to the same canonical URL will not match within datastore.
type datastore map[string]interface{}
// Get retrieves contents stored at path and saves it to data. If nothing exists at path,
// an error satisfying errors.IsNotFound is returned.
func (d datastore) Get(path string, data interface{}) error {
current := d[path]
if current == nil {
return errors.NotFoundf(path)
}
data = current
return nil
}
// Put stores data at path. It will be accessible later via Get.
// Data already at path will is overwritten and no
// revision history is saved.
func (d datastore) Put(path string, data interface{}) error {
d[path] = data
return nil
}
// PutReader data from a an io.Reader at path. It will be accessible later via Get.
// Data already at path will is overwritten and no
// revision history is saved.
func (d *datastore) PutReader(path string, data io.Reader) error {
buffer := []byte{}
_, err := data.Read(buffer)
if err != nil {
return errors.Trace(err)
}
return d.Put(path, buffer)
}
// FakeClient is a stand-in for the gopkg.in/juju/charmrepo.v3/csclient Client type.
// Its "stores" data within several an in-memory map for each object that charmstores know about, primarily charms, bundles and resources.
//
// An abridged session would look something like this, where charmId is a *charm.URL:
// // initialise a new charmstore with an empty repository
// repo := NewRepository()
// client := NewFakeClient(repo)
// client.UploadCharm(charmId)
// // later on
// charm := client.Get(charmId)
type FakeClient struct {
repo *Repository
}
//var _ csWrapper = (*FakeClient)(nil)
// NewFakeClient returns a FakeClient that is initialised
// with repo. If repo is nil, a blank Repository will be
// created.
func NewFakeClient(repo *Repository) *FakeClient {
if repo == nil {
repo = NewRepository()
}
return &FakeClient{repo}
}
// Get retrieves data from path. If nothing has been Put to
// path, an error satisfying errors.IsNotFound is returned.
func (c FakeClient) Get(path string, value interface{}) error {
return c.repo.resourcesData.Get(path, value)
}
// WithChannel returns a ChannelAwareFakeClient with its channel
// set to channel and its other values originating from this client.
func (c FakeClient) WithChannel(channel params.Channel) *ChannelAwareFakeClient {
return &ChannelAwareFakeClient{channel, c}
}
// Put uploads data to path, overwriting any data that is already present
func (c FakeClient) Put(path string, value interface{}) error {
return c.repo.resourcesData.Put(path, value)
}
// AddDockerResource adds a docker resource against id.
func (c FakeClient) AddDockerResource(id *charm.URL, resourceName string, imageName, digest string) (revision int, err error) {
return -1, nil
}
// UploadCharm takes a charm's formal identifier (its URL)
// and its contents, then uploads it into the store for other clients
// to download.
//
// Returns another charm identifier, which has had its revision set to
// the new revision, which will be 0 for the first revision or
// current revision in the store, plus 1.
func (c FakeClient) UploadCharm(id *charm.URL, charmData charm.Charm) (*charm.URL, error) {
return c.repo.UploadCharm(id, charmData)
}
// UploadCharmWithRevision takes a charm's formal identifier (its URL)
// and its contents and a revision number, then uploads it into the
// store for other clients to download.
func (c FakeClient) UploadCharmWithRevision(id *charm.URL, charmData charm.Charm, promulgatedRevision int) error {
return c.repo.UploadCharmWithRevision(id, charmData, promulgatedRevision)
}
// UploadBundle takes a bundle's formal identifier (its URL)
// and its contents, then uploads it into the store for other clients
// to download.
//
// Returns another charm identifier, which has had its revision set to
// the new revision, which will be 0 for the first revision or
// current revision in the store, plus 1.
func (c FakeClient) UploadBundle(id *charm.URL, bundleData charm.Bundle) (*charm.URL, error) {
return c.repo.UploadBundle(id, bundleData)
}
// UploadBundleWithRevision takes a bundle's formal identifier (its URL)
// and its contents and a revision number, then uploads it into the
// store for other clients to download.
func (c FakeClient) UploadBundleWithRevision(id *charm.URL, bundleData charm.Bundle, promulgatedRevision int) error {
return c.repo.UploadBundleWithRevision(id, bundleData, promulgatedRevision)
}
// UploadResource uploads a resource (an archive) into the store,
// tags it to a charm/bundle represented by id for other clients
// to download when they download the charm/bundle.
//
// In this implementation, the progress parameter is ignored.
func (c FakeClient) UploadResource(id *charm.URL, name, path string, file io.ReaderAt, size int64, progress csclient.Progress) (revision int, err error) {
return c.repo.UploadResource(id, name, path, file, size, progress)
}
// ListResources returns Resource metadata for resources that have been
// uploaded to the repository for id. To upload a resource, use UploadResource.
// Although id is type *charm.URL, resources are not restricted to charms. That
// type is also used for other entities in the charmstore, such as bundles.
//
// Returns an error that satisfies errors.IsNotFound when no resources
// are present for id.
func (c FakeClient) ListResources(channel params.Channel, id *charm.URL) ([]params.Resource, error) {
originalChannel := c.repo.channel
defer func() { c.repo.channel = originalChannel }()
c.repo.channel = channel
return c.repo.ListResources(id)
}
// Publish marks id as published against channels within the charm store.
//
// In this implementation, the resources parameter is ignored.
func (c FakeClient) Publish(id *charm.URL, channels []params.Channel, resources map[string]int) error {
return c.repo.Publish(id, channels, resources)
}
// ChannelAwareFakeClient is a charmstore client that stores the channel that its methods
// refer to across calls. That is, it is stateful. It is modelled on the Client type defined in
// gopkg.in/juju/charmrepo.v3/csclient.
//
// Constructing ChannelAwareFakeClient
//
// ChannelAwareFakeClient does not have a NewChannelAwareFakeClient method. To construct an
// instance, use the following pattern:
// NewFakeClient(nil).WithChannel(channel)
//
// Setting the channel
//
// To set ChannelAwareFakeClient's channel, its the WithChannel method.
type ChannelAwareFakeClient struct {
channel params.Channel
charmstore FakeClient
}
// Get retrieves data from path. If nothing has been Put to
// path, an error satisfying errors.IsNotFound is returned.
func (c ChannelAwareFakeClient) Get(path string, value interface{}) error {
return c.charmstore.Get(path, value)
}
// Put uploads data to path, overwriting any data that is already present
func (c ChannelAwareFakeClient) Put(path string, value interface{}) error {
return c.charmstore.Put(path, value)
}
// AddDockerResource adds a docker resource against id.
func (c ChannelAwareFakeClient) AddDockerResource(id *charm.URL, resourceName string, imageName, digest string) (revision int, err error) {
return c.charmstore.AddDockerResource(id, resourceName, imageName, digest)
}
// UploadCharm takes a charm's formal identifier (its URL)
// and its contents, then uploads it into the store for other clients
// to download.
//
// Returns another charm identifier, which has had its revision set to
// the new revision, which will be 0 for the first revision or
// current revision in the store, plus 1.
func (c ChannelAwareFakeClient) UploadCharm(id *charm.URL, ch charm.Charm) (*charm.URL, error) {
return c.charmstore.UploadCharm(id, ch)
}
// UploadCharmWithRevision takes a charm's formal identifier (its URL)
// and its contents and a revision number, then uploads it into the
// store for other clients to download.
func (c ChannelAwareFakeClient) UploadCharmWithRevision(id *charm.URL, ch charm.Charm, promulgatedRevision int) error {
return c.charmstore.UploadCharmWithRevision(id, ch, promulgatedRevision)
}
// UploadBundle takes a bundle's formal identifier (its URL)
// and its contents, then uploads it into the store for other clients
// to download.
//
// Returns another charm identifier, which has had its revision set to
// the new revision, which will be 0 for the first revision or
// current revision in the store, plus 1.
func (c ChannelAwareFakeClient) UploadBundle(id *charm.URL, bundle charm.Bundle) (*charm.URL, error) {
return c.charmstore.UploadBundle(id, bundle)
}
// UploadBundleWithRevision takes a bundle's formal identifier (its URL)
// and its contents and a revision number, then uploads it into the
// store for other clients to download.
func (c ChannelAwareFakeClient) UploadBundleWithRevision(id *charm.URL, bundle charm.Bundle, promulgatedRevision int) error {
return c.charmstore.UploadBundleWithRevision(id, bundle, promulgatedRevision)
}
// UploadResource uploads a resource (an archive) into the store,
// tags it to a charm/bundle represented by id for other clients
// to download when they download the charm/bundle.
//
// In this implementation, the progress parameter is ignored.
func (c ChannelAwareFakeClient) UploadResource(id *charm.URL, name, path string, file io.ReaderAt, size int64, progress csclient.Progress) (revision int, err error) {
return c.charmstore.UploadResource(id, name, path, file, size, progress)
}
// ListResources returns Resource metadata for resources that have been
// uploaded to the repository for id. To upload a resource, use UploadResource.
// Although id is type *charm.URL, resources are not restricted to charms. That
// type is also used for other entities in the charmstore, such as bundles.
//
// Returns an error that satisfies errors.IsNotFound when no resources
// are present for id.
func (c ChannelAwareFakeClient) ListResources(id *charm.URL) ([]params.Resource, error) {
return c.charmstore.ListResources(c.channel, id)
}
// Publish marks id as published against channels within the charm store.
//
// In this implementation, the resources parameter is ignored.
func (c ChannelAwareFakeClient) Publish(id *charm.URL, channels []params.Channel, resources map[string]int) error {
return c.charmstore.Publish(id, channels, resources)
}
// WithChannel returns a ChannelAwareFakeClient with its channel set to channel.
func (c ChannelAwareFakeClient) WithChannel(channel params.Channel) *ChannelAwareFakeClient {
c.channel = channel
return &c
}
// Repository provides in-memory access to charms and other objects
// held in a charmstore (or locally), such as bundles and resources.
// Its intended use case is to act as a fake charmrepo for testing purposes.
//
// Warnings
//
// No guarantees are made that Repository maintains its invariants or that the behaviour
// matches the behaviour of the actual charm store. For example, Repository's information
// about which charm revisions it knows about is decoupled from the charm data that it currently
// stores.
//
// Related Interfaces
//
// Repository implements gopkg.in/juju/charmrepo Interface and derivative
// interfaces, such as github.com/juju/juju/cmd/juju/application DeployAPI.
type Repository struct {
channel params.Channel
charms map[params.Channel]map[charm.URL]charm.Charm
bundles map[params.Channel]map[charm.URL]charm.Bundle
resources map[params.Channel]map[charm.URL][]params.Resource
revisions map[params.Channel]map[charm.URL]int
added map[string][]charm.URL
resourcesData datastore
generations map[string]string
published map[params.Channel]set.Strings
}
// NewRepository returns an empty Repository. To populate it with charms, bundles and resources
// use UploadCharm, UploadBundle and/or UploadResource.
func NewRepository() *Repository {
repo := Repository{
channel: params.StableChannel,
charms: make(map[params.Channel]map[charm.URL]charm.Charm),
bundles: make(map[params.Channel]map[charm.URL]charm.Bundle),
resources: make(map[params.Channel]map[charm.URL][]params.Resource),
revisions: make(map[params.Channel]map[charm.URL]int),
added: make(map[string][]charm.URL),
resourcesData: make(datastore),
published: make(map[params.Channel]set.Strings),
}
for _, channel := range params.OrderedChannels {
repo.charms[channel] = make(map[charm.URL]charm.Charm)
repo.bundles[channel] = make(map[charm.URL]charm.Bundle)
repo.resources[channel] = make(map[charm.URL][]params.Resource)
repo.revisions[channel] = make(map[charm.URL]int)
repo.published[channel] = set.NewStrings()
}
return &repo
}
func (r *Repository) addRevision(ref *charm.URL) *charm.URL {
revision := r.revisions[r.channel][*ref]
return ref.WithRevision(revision)
}
// AddCharm registers a charm's availability on a particular channel,
// but does not upload its contents. This is part of a two stage process
// for storing charms in the repository.
//
// In this implementation, the force parameter is ignored.
func (r Repository) AddCharm(id *charm.URL, channel params.Channel, force bool) error {
withRevision := r.addRevision(id)
alreadyAdded := r.added[string(channel)]
for _, charm := range alreadyAdded {
if *withRevision == charm {
return nil
// TODO(tsm) check expected behaviour
//
// if force {
// return nil
// } else {
// return errors.NewAlreadyExists(errors.NewErr("%v already added in channel %v", id, channel))
// }
}
}
r.added[string(channel)] = append(alreadyAdded, *withRevision)
return nil
}
// AddCharmWithAuthorization is equivalent to AddCharm.
// The macaroon parameter is ignored.
func (r Repository) AddCharmWithAuthorization(id *charm.URL, channel params.Channel, macaroon *macaroon.Macaroon, force bool) error {
return r.AddCharm(id, channel, force)
}
// AddLocalCharm allows you to register a charm that is not associated with a particular release channel.
// Its purpose is to facilitate registering charms that have been built locally.
func (r Repository) AddLocalCharm(id *charm.URL, details charm.Charm, force bool) (*charm.URL, error) {
return id, r.AddCharm(id, params.NoChannel, force)
}
// AuthorizeCharmstoreEntity returns (nil,nil) as Repository
// has no authorisation to manage
func (r Repository) AuthorizeCharmstoreEntity(id *charm.URL) (*macaroon.Macaroon, error) {
return nil, nil
}
// CharmInfo returns information about charms that are currently in the charm store.
func (r Repository) CharmInfo(charmURL string) (*charms.CharmInfo, error) {
charmId, err := charm.ParseURL(charmURL)
if err != nil {
return nil, errors.Trace(err)
}
charmDetails, err := r.Get(charmId)
if err != nil {
return nil, errors.Trace(err)
}
info := charms.CharmInfo{
Revision: charmDetails.Revision(),
URL: charmId.String(),
Config: charmDetails.Config(),
Meta: charmDetails.Meta(),
Actions: charmDetails.Actions(),
Metrics: charmDetails.Metrics(),
}
return &info, nil
}
// Resolve disambiguates a charm to a specific revision.
//
// Part of the charmrepo.Interface
func (r Repository) Resolve(ref *charm.URL) (canonRef *charm.URL, supportedSeries []string, err error) {
return r.addRevision(ref), []string{"trusty", "wily", "quantal"}, nil
}
// ResolveWithChannel disambiguates a charm to a specific revision.
//
// Part of the cmd/juju/application.DeployAPI interface
func (r Repository) ResolveWithChannel(ref *charm.URL) (*charm.URL, params.Channel, []string, error) {
canonRef, supportedSeries, err := r.Resolve(ref)
return canonRef, r.channel, supportedSeries, err
}
// Get retrieves a charm from the repository.
//
// Part of the charmrepo.Interface
func (r Repository) Get(id *charm.URL) (charm.Charm, error) {
withRevision := r.addRevision(id)
charmData := r.charms[r.channel][*withRevision]
if charmData == nil {
return charmData, errors.NotFoundf("cannot retrieve \"%v\": charm", id.String())
}
return charmData, nil
}
// GetBundle retrieves a bundle from the repository.
//
// Part of the charmrepo.Interface
func (r Repository) GetBundle(id *charm.URL) (charm.Bundle, error) {
bundleData := r.bundles[r.channel][*id]
if bundleData == nil {
return nil, errors.NotFoundf(id.String())
}
return bundleData, nil
}
// ListResources returns Resource metadata for resources that have been
// uploaded to the repository for id. To upload a resource, use UploadResource.
// Although id is type *charm.URL, resources are not restricted to charms. That
// type is also used for other entities in the charmstore, such as bundles.
//
// Returns an error that satisfies errors.IsNotFound when no resources
// are present for id.
func (r Repository) ListResources(id *charm.URL) ([]params.Resource, error) {
resources := r.resources[r.channel][*id]
if len(resources) == 0 {
return resources, errors.NotFoundf("no resources for %v", id)
}
return resources, nil
}
// UploadCharm takes a charm's formal identifier (its URL)
// and its contents, then uploads it into the store for clients
// to download.
//
// Returns another charm identifier, which has had its revision set to
// the new revision, which will be 0 for the first revision or
// current revision in the store, plus 1.
func (r Repository) UploadCharm(id *charm.URL, charmData charm.Charm) (*charm.URL, error) {
if len(r.charms[r.channel]) == 0 {
r.charms[r.channel] = make(map[charm.URL]charm.Charm)
}
withRevision := r.addRevision(id)
r.charms[r.channel][*withRevision] = charmData
return withRevision, nil
}
// UploadCharmWithRevision takes a charm's formal identifier (its URL)
// and its contents and a revision number, then uploads it into the
// store for clients to download.
func (r Repository) UploadCharmWithRevision(id *charm.URL, charmData charm.Charm, promulgatedRevision int) error {
if len(r.revisions[r.channel]) == 0 {
r.revisions[r.channel] = make(map[charm.URL]int)
}
r.revisions[r.channel][*id] = promulgatedRevision
_, err := r.UploadCharm(id, charmData)
if err != nil {
return errors.Trace(err)
}
return nil
}
// UploadBundle takes a bundle's formal identifier (its URL)
// and its contents, then uploads it into the store for other clients
// to download.
//
// Returns another charm identifier, which has had its revision set to
// the new revision, which will be 0 for the first revision or
// current revision in the store, plus 1.
func (r Repository) UploadBundle(id *charm.URL, bundleData charm.Bundle) (*charm.URL, error) {
r.bundles[r.channel][*id] = bundleData
return id, nil
}
// UploadBundleWithRevision takes a bundle's formal identifier (its URL)
// and its contents and a revision number, then uploads it into the
// store for other clients to download.
func (r Repository) UploadBundleWithRevision(id *charm.URL, bundleData charm.Bundle, promulgatedRevision int) error {
_, err := r.UploadBundle(id, bundleData)
if err != nil {
return errors.Trace(err)
}
r.revisions[r.channel][*id] = promulgatedRevision
return nil
}
// UploadResource uploads a resource (an archive) into the store,
// tags it to a charm/bundle represented by id for other clients
// to download when they download the charm/bundle.
//
// In this implementation, the progress parameter is ignored.
func (r Repository) UploadResource(id *charm.URL, name, path string, file io.ReaderAt, size int64, progress csclient.Progress) (revision int, err error) {
if len(r.resources[r.channel]) == 0 {
r.resources[r.channel] = make(map[charm.URL][]params.Resource)
}
resources, err := r.ListResources(id)
if err != nil {
return -1, errors.Trace(err)
}
revision = len(resources)
data := []byte{}
_, err = file.ReadAt(data, 0)
if err != nil {
return -1, errors.Trace(err)
}
hash, err := signature(bytes.NewBuffer(data))
if err != nil {
return -1, errors.Trace(err)
}
r.resources[r.channel][*id] = append(resources, params.Resource{
Name: name,
Path: path,
Revision: revision,
Size: size,
Fingerprint: hash,
})
err = r.resourcesData.Put(path, data)
if err != nil {
return -1, errors.Trace(err)
}
return revision, nil
}
// Publish marks a charm or bundle as published within channels.
//
// In this implementation, the resources parameter is ignored.
func (r Repository) Publish(id *charm.URL, channels []params.Channel, resources map[string]int) error {
for _, channel := range channels {
published := r.published[channel]
published.Add(id.String())
r.published[channel] = published
}
return nil
}
// signature creates a SHA384 digest from r
func signature(r io.Reader) (hash []byte, err error) {
h := sha512.New384()
_, err = io.Copy(h, r)
if err != nil {
return nil, errors.Trace(err)
}
hash = []byte(fmt.Sprintf("%x", h.Sum(nil)))
return hash, nil
}