Merge lp:~gary/juju-gui/fakebackend-1 into lp:juju-gui/experimental

Proposed by Gary Poster
Status: Merged
Merged at revision: 422
Proposed branch: lp:~gary/juju-gui/fakebackend-1
Merge into: lp:juju-gui/experimental
Diff against target: 845 lines (+738/-18)
8 files modified
.lbox (+1/-0)
app/models/charm.js (+39/-17)
app/modules-debug.js (+4/-0)
app/store/env/fakebackend.js (+336/-0)
test/data/wordpress-charmdata.json (+63/-0)
test/index.html (+1/-0)
test/test_fakebackend.js (+292/-0)
test/test_service_view.js (+2/-1)
To merge this branch: bzr merge lp:~gary/juju-gui/fakebackend-1
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+152696@code.launchpad.net

Description of the change

First components of in-memory juju environment

This branch adds the first part of an in-memory juju environment. Next steps include machine generation to addUnit, adding configFile support to deploy (js-yaml has made some big strides), adding code that can report changes (for the delta stream/watcher protocols), writing a first cut of the Python wire protocol, and writing a first cut of the Go protocol. After that we should be able to just add in remaining commands.

This was driven by a prototype by Kapil, and with Benji's help.

https://codereview.appspot.com/7641051/

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Reviewers: mp+152696_code.launchpad.net,

Message:
Please take a look.

Description:
First components of in-memory juju environment

This branch adds the first part of an in-memory juju environment. Next
steps include machine generation to addUnit, adding configFile support
to deploy (js-yaml has made some big strides), adding code that can
report changes (for the delta stream/watcher protocols), writing a first
cut of the Python wire protocol, and writing a first cut of the Go
protocol. After that we should be able to just add in remaining
commands.

This was driven by a prototype by Kapil, and with Benji's help.

https://code.launchpad.net/~gary/juju-gui/fakebackend-1/+merge/152696

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/7641051/

Affected files:
   A .lbox
   A [revision details]
   M app/models/charm.js
   M app/modules-debug.js
   A app/store/env/fakebackend.js
   A test/data/wordpress-charmdata.json
   M test/index.html
   A test/test_fakebackend.js
   M test/test_service_view.js

Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM

I don't love the name 'fakebackend', juju-improv or juju-memory-env or
something make read better, but its really up to you.

The env is simple to follow and a nicely captures the rules around these
first juju interactions. Given that you started with the most involved
call (I think) it should be easy to get the rest working.

Thanks

https://codereview.appspot.com/7641051/diff/1/app/models/charm.js
File app/models/charm.js (right):

https://codereview.appspot.com/7641051/diff/1/app/models/charm.js#newcode45
app/models/charm.js:45: };
Nice cleanup

https://codereview.appspot.com/7641051/diff/1/app/store/env/fakebackend.js
File app/store/env/fakebackend.js (right):

https://codereview.appspot.com/7641051/diff/1/app/store/env/fakebackend.js#newcode42
app/store/env/fakebackend.js:42: this.db = new models.Database();
It took some thinking to convince me that we don't want to be able to
inject the database from outside, but I think I've come around now.

https://codereview.appspot.com/7641051/diff/1/test/test_fakebackend.js
File test/test_fakebackend.js (right):

https://codereview.appspot.com/7641051/diff/1/test/test_fakebackend.js#newcode42
test/test_fakebackend.js:42: 'data/' + name + '-charmdata.json', {sync:
true}).responseText });
There is a method for this in test endpoints that should most likely be
migrated to test/utils. loadFixture.

   function loadFixture(url) {
         return Y.JSON.parse(Y.io(url, {sync: true}).responseText);
       }
       sample_env = loadFixture('data/large_stream.json');
       sample_endpoints = loadFixture('data/large_endpoints.json');

I think its better to reuse this even if its a line or two.

https://codereview.appspot.com/7641051/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

LGTM

Thanks for this, really good addition.

https://codereview.appspot.com/7641051/

Revision history for this message
Gary Poster (gary) wrote :

On 2013/03/11 16:05:18, bcsaller wrote:
> LGTM

> I don't love the name 'fakebackend', juju-improv or juju-memory-env or
something
> make read better, but its really up to you.

I agree that it is not fantastic, but I felt "fakebackend" conveyed the
intent better than these or other options I came up with.

My thought is that we can go back and revise this later if desired. If
you made the argument that "memory-env" or similar gave people a better
idea of intended usage then I'd be happy to come around behind and
change it. ("improv" has never conveyed the right meaning to me, tbh.
PlaygroundEnv? SandboxEnv?)

Argue passionately for a name and I'll make a separate branch with the
change. :-)

> The env is simple to follow and a nicely captures the rules around
these first
> juju interactions. Given that you started with the most involved call
(I think)
> it should be easy to get the rest working.

> Thanks

> https://codereview.appspot.com/7641051/diff/1/app/models/charm.js
> File app/models/charm.js (right):

https://codereview.appspot.com/7641051/diff/1/app/models/charm.js#newcode45
> app/models/charm.js:45: };
> Nice cleanup

Thanks.

https://codereview.appspot.com/7641051/diff/1/app/store/env/fakebackend.js
> File app/store/env/fakebackend.js (right):

https://codereview.appspot.com/7641051/diff/1/app/store/env/fakebackend.js#newcode42
> app/store/env/fakebackend.js:42: this.db = new models.Database();
> It took some thinking to convince me that we don't want to be able to
inject the
> database from outside, but I think I've come around now.

Cool.

https://codereview.appspot.com/7641051/diff/1/test/test_fakebackend.js
> File test/test_fakebackend.js (right):

https://codereview.appspot.com/7641051/diff/1/test/test_fakebackend.js#newcode42
> test/test_fakebackend.js:42: 'data/' + name + '-charmdata.json',
{sync:
> true}).responseText });
> There is a method for this in test endpoints that should most likely
be migrated
> to test/utils. loadFixture.

> function loadFixture(url) {
> return Y.JSON.parse(Y.io(url, {sync: true}).responseText);
> }
> sample_env = loadFixture('data/large_stream.json');
> sample_endpoints = loadFixture('data/large_endpoints.json');

> I think its better to reuse this even if its a line or two.

Yes, I looked at that. The reason why I didn't reuse it was that I was
really only reusing the Y.io sync call: you parse JSON and pull the
responseText out, and I actually need the full responseText string,
because that's what the charm store expects. That said, as I was
considering your suggestion, I realized that I could simplify. If you
look at my new branch, you should see a smaller function that more
clearly shows the differences.

Thank you, to both you and Matt!

Gary

https://codereview.appspot.com/7641051/

Revision history for this message
Gary Poster (gary) wrote :

*** Submitted:

First components of in-memory juju environment

This branch adds the first part of an in-memory juju environment. Next
steps include machine generation to addUnit, adding configFile support
to deploy (js-yaml has made some big strides), adding code that can
report changes (for the delta stream/watcher protocols), writing a first
cut of the Python wire protocol, and writing a first cut of the Go
protocol. After that we should be able to just add in remaining
commands.

This was driven by a prototype by Kapil, and with Benji's help.

R=bcsaller, matthew.scott
CC=
https://codereview.appspot.com/7641051

https://codereview.appspot.com/7641051/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.lbox'
2--- .lbox 1970-01-01 00:00:00 +0000
3+++ .lbox 2013-03-11 15:03:24 +0000
4@@ -0,0 +1,1 @@
5+propose -cr -for lp:juju-gui
6
7=== modified file 'app/models/charm.js'
8--- app/models/charm.js 2013-02-08 17:36:58 +0000
9+++ app/models/charm.js 2013-03-11 15:03:24 +0000
10@@ -12,6 +12,37 @@
11 var models = Y.namespace('juju.models');
12 var charmIdRe = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+)$/;
13 var idElements = ['scheme', 'owner', 'series', 'package_name', 'revision'];
14+ var simpleCharmIdRe = /^(?:(\w+):)?(?!:~)(\w+)$/;
15+ var simpleIdElements = ['scheme', 'package_name'];
16+ var parseCharmId = models.parseCharmId = function(charmId, defaultSeries) {
17+ if (Y.Lang.isString(charmId)) {
18+ var parts = charmIdRe.exec(charmId);
19+ var pairs;
20+ if (parts) {
21+ parts.shift(); // Get rid of the first, full string.
22+ pairs = Y.Array.zip(idElements, parts);
23+ } else if (defaultSeries) {
24+ parts = simpleCharmIdRe.exec(charmId);
25+ if (parts) {
26+ parts.shift(); // Get rid of the first, full string.
27+ pairs = Y.Array.zip(simpleIdElements, parts);
28+ pairs.push(['series', defaultSeries]);
29+ }
30+ }
31+ if (parts) {
32+ var result = {};
33+ Y.Array.map(pairs, function(pair) { result[pair[0]] = pair[1]; });
34+ result.charm_store_path = [
35+ (result.owner ? '~' + result.owner : 'charms'),
36+ result.series,
37+ result.package_name + (
38+ result.revision ? '-' + result.revision : ''),
39+ 'json'
40+ ].join('/');
41+ return result;
42+ }
43+ }
44+ };
45
46 /**
47 * Charms, once instantiated and loaded with data from their respective
48@@ -49,32 +80,23 @@
49
50 initializer: function() {
51 var id = this.get('id'),
52- parts = id && charmIdRe.exec(id),
53+ parts = parseCharmId(id),
54 self = this;
55- if (!Y.Lang.isValue(id) || !parts) {
56+ if (!parts) {
57 throw 'Developers must initialize charms with a well-formed id.';
58 }
59 this.loaded = false;
60 this.on('load', function() { this.loaded = true; });
61- parts.shift();
62- Y.each(
63- Y.Array.zip(idElements, parts),
64- function(pair) { self.set(pair[0], pair[1]); });
65+ Y.Object.each(
66+ parts,
67+ function(value, key) { self.set(key, value); });
68 // full_name
69 var tmp = [this.get('series'), this.get('package_name')],
70- owner = this.get('owner');
71+ owner = this.get('owner');
72 if (owner) {
73 tmp.unshift('~' + owner);
74 }
75 this.set('full_name', tmp.join('/'));
76- // charm_store_path
77- this.set(
78- 'charm_store_path',
79- [(owner ? '~' + owner : 'charms'),
80- this.get('series'),
81- (this.get('package_name') + '-' + this.get('revision')),
82- 'json'
83- ].join('/'));
84 },
85
86 sync: function(action, options, callback) {
87@@ -120,8 +142,8 @@
88 data.is_subordinate = data.subordinate;
89 Y.each(data, function(value, key) {
90 if (!value ||
91- !self.attrAdded(key) ||
92- Y.Lang.isValue(self.get(key))) {
93+ !self.attrAdded(key) ||
94+ Y.Lang.isValue(self.get(key))) {
95 delete data[key];
96 }
97 });
98
99=== modified file 'app/modules-debug.js'
100--- app/modules-debug.js 2013-03-08 20:55:42 +0000
101+++ app/modules-debug.js 2013-03-11 15:03:24 +0000
102@@ -189,6 +189,10 @@
103 fullpath: '/juju-ui/store/env/python.js'
104 },
105
106+ 'juju-env-fakebackend': {
107+ fullpath: '/juju-ui/store/env/fakebackend.js'
108+ },
109+
110 'juju-notification-controller': {
111 fullpath: '/juju-ui/store/notifications.js'
112 },
113
114=== added file 'app/store/env/fakebackend.js'
115--- app/store/env/fakebackend.js 1970-01-01 00:00:00 +0000
116+++ app/store/env/fakebackend.js 2013-03-11 15:03:24 +0000
117@@ -0,0 +1,336 @@
118+'use strict';
119+
120+/**
121+ * An in-memory fake Juju backend and supporting elements.
122+ *
123+ * @module env
124+ * @submodule env.fakebackend
125+ */
126+
127+YUI.add('juju-env-fakebackend', function(Y) {
128+
129+ var models = Y.namespace('juju.models');
130+ var UNAUTHENTICATEDERROR = {error: 'Please log in.'};
131+ /**
132+ * An in-memory fake Juju backend.
133+ *
134+ * @class FakeBackend
135+ */
136+ function FakeBackend(config) {
137+ // Invoke Base constructor, passing through arguments.
138+ FakeBackend.superclass.constructor.apply(this, arguments);
139+ }
140+
141+ FakeBackend.NAME = 'fake-backend';
142+ FakeBackend.ATTRS = {
143+ authorizedUsers: {value: {'admin': 'password'}},
144+ authenticated: {value: false},
145+ charmStore: {}, // Required.
146+ defaultSeries: {value: 'precise'},
147+ providerType: {value: 'demonstration'}
148+ };
149+
150+ Y.extend(FakeBackend, Y.Base, {
151+
152+ /**
153+ Initializes.
154+
155+ @method initializer
156+ @return {undefined} Nothing.
157+ */
158+ initializer: function() {
159+ this.db = new models.Database();
160+ },
161+
162+ /**
163+ Attempt to log a user in.
164+
165+ @method login
166+ @param {String} username The id of the user.
167+ @param {String} submittedPassword The user-submitted password.
168+ @return {Bool} True if the authentication was successful.
169+ */
170+ login: function(username, submittedPassword) {
171+ var password = this.get('authorizedUsers')[username],
172+ authenticated = password === submittedPassword;
173+ this.set('authenticated', authenticated);
174+ return authenticated;
175+ },
176+
177+ /**
178+ Log out. If already logged out, no error is raised.
179+ */
180+ logout: function() {
181+ this.set('authenticated', false);
182+ },
183+
184+ /**
185+ Deploy a charm. Uses a callback for response!
186+
187+ @method deploy
188+ @param {String} charmUrl The URL of the charm.
189+ @param {Function} callback A call that will receive an object either
190+ with an "error" attribute containing a string describing the problem,
191+ or with a "service" attribute containing the new service, a "charm"
192+ attribute containing the charm used, and a "units" attribute
193+ containing a list of the added units. This is asynchronous because we
194+ often must go over the network to the charm store.
195+ @param {Object} options An options object.
196+ name: The name of the service to be deployed, defaulting to the charm
197+ name.
198+ config: The charm configuration options, defaulting to none.
199+ unitCount: The number of units to be deployed.
200+ @return {undefined} Get the result from the callback.
201+ */
202+ deploy: function(charmId, callback, options) {
203+ if (!this.get('authenticated')) {
204+ return callback(UNAUTHENTICATEDERROR);
205+ }
206+ if (!options) {
207+ options = {};
208+ }
209+ var self = this;
210+ this._loadCharm(
211+ charmId,
212+ {
213+ /**
214+ Deploy the successfully-obtained charm.
215+ */
216+ success: function(charm) {
217+ self._deployFromCharm(charm, callback, options);
218+ },
219+ failure: callback
220+ }
221+ );
222+ },
223+
224+ /**
225+ Get a charm from a URL, via charmStore and/or db. Uses callbacks.
226+
227+ @method _loadCharm
228+ @param {String} charmUrl The URL of the charm.
229+ @param {Function} callbacks An optional object with optional success and
230+ failure callables. This is asynchronous because we
231+ often must go over the network to the charm store. The success
232+ callable receives the fully loaded charm, and the failure callable
233+ receives an object with an explanatory "error" attribute.
234+ @return {undefined} Use the callbacks to handle success or failure.
235+ */
236+ _loadCharm: function(charmId, callbacks) {
237+ var charmIdParts = models.parseCharmId(
238+ charmId, this.get('defaultSeries'));
239+ if (!callbacks) {
240+ callbacks = {};
241+ }
242+ if (!charmIdParts) {
243+ return (
244+ callbacks.failure &&
245+ callbacks.failure({error: 'Invalid charm id.'}));
246+ }
247+ var charm = this.db.charms.getById(charmId);
248+ if (charm) {
249+ callbacks.success(charm);
250+ } else {
251+ // Get the charm data.
252+ var self = this;
253+ this.get('charmStore').loadByPath(
254+ charmIdParts.charm_store_path,
255+ {
256+ /**
257+ Convert the charm data to a charm and use the success callback.
258+ */
259+ success: function(data) {
260+ var charm = self._getCharmFromData(data);
261+ if (callbacks.success) {
262+ callbacks.success(charm);
263+ }
264+ },
265+ /**
266+ Inform the caller of an error using the charm store.
267+ */
268+ failure: function(e) {
269+ if (callbacks.failure) {
270+ callbacks.failure({error: 'Could not contact charm store.'});
271+ }
272+ }
273+ }
274+ );
275+ }
276+
277+ },
278+
279+ /**
280+ Convert charm data as returned by the charmStore into a charm.
281+ The charm might be pre-existing or might need to be created, but
282+ after this method it will be within the db.
283+
284+ @method _getCharmFromData
285+ @param {Object} data The raw charm information as delivered by the
286+ charmStore's loadByPath method.
287+ @return {Object} A matching charm from the db.
288+ */
289+ _getCharmFromData: function(data) {
290+ var charm = this.db.charms.getById(data.store_url);
291+ if (!charm) {
292+ delete data.store_revision;
293+ delete data.bzr_branch;
294+ delete data.last_change;
295+ data.id = data.store_url;
296+ charm = this.db.charms.add(data);
297+ }
298+ return charm;
299+ },
300+
301+ /**
302+ Deploy a charm, given the charm, a callback, and options.
303+
304+ @param {Object} charm The charm to be deployed, from the db.
305+ @param {Function} callback A call that will receive an object either
306+ with an "error" attribute containing a string describing the problem,
307+ or with a "service" attribute containing the new service, a "charm"
308+ attribute containing the charm used, and a "units" attribute
309+ containing a list of the added units. This is asynchronous because we
310+ often must go over the network to the charm store.
311+ @param {Object} options An options object.
312+ name: The name of the service to be deployed, defaulting to the charm
313+ name.
314+ config: The charm configuration options, defaulting to none.
315+ unitCount: The number of units to be deployed.
316+ @return {undefined} Get the result from the callback.
317+ */
318+ _deployFromCharm: function(charm, callback, options) {
319+ if (!options.name) {
320+ options.name = charm.get('package_name');
321+ }
322+ if (this.db.services.getById(options.name)) {
323+ return callback({error: 'A service with this name already exists.'});
324+ }
325+ var service = this.db.services.add({
326+ id: options.name,
327+ name: options.name,
328+ charm: charm.get('id'),
329+ exposed: false,
330+ subordinate: charm.get('is_subordinate'),
331+ config: options.config
332+ });
333+ var response = this.addUnit(options.name, options.unitCount);
334+ response.service = service;
335+ callback(response);
336+ },
337+
338+ // destroyService: function() {
339+
340+ // },
341+
342+ // getService: function() {
343+
344+ // },
345+
346+ // getCharm: function() {
347+
348+ // },
349+
350+ /**
351+ Add units to the given service.
352+
353+ @method addUnit
354+ @param {String} serviceName The name of the service to be scaled up.
355+ @param {Integer} numUnits The number of units to be added, defaulting
356+ to 1.
357+ @return {Object} Returns an object either with an "error" attribute
358+ containing a string describing the problem, or with a "units"
359+ attribute containing a list of the added units.
360+ */
361+ addUnit: function(serviceName, numUnits) {
362+ if (!this.get('authenticated')) {
363+ return UNAUTHENTICATEDERROR;
364+ }
365+ if (Y.Lang.isUndefined(numUnits)) {
366+ numUnits = 1;
367+ }
368+ if (!Y.Lang.isNumber(numUnits) || numUnits < 1) {
369+ return {error: 'Invalid number of units.'};
370+ }
371+ var service = this.db.services.getById(serviceName);
372+ if (!service) {
373+ return {error: 'Service "' + serviceName + '" does not exist.'};
374+ }
375+ if (!service.unitSequence) {
376+ service.unitSequence = 0;
377+ }
378+ var result = [];
379+ // var unitMachines = this._getUnitMachines(numUnits);
380+
381+ for (var i = 0; i < numUnits; i += 1) {
382+ var unitId = service.unitSequence += 1;
383+ result.push(
384+ this.db.units.add({
385+ 'id': serviceName + '/' + unitId,
386+ //'machine': unit_machines.shift(),
387+ // The models use underlines, not hyphens (see
388+ // app/models/models.js in _process_delta.)
389+ 'agent_state': 'started'
390+ })
391+ );
392+ }
393+ return {units: result};
394+ } // ,
395+
396+ // removeUnit: function() {
397+
398+ // },
399+
400+ // getEndpoints: function() {
401+
402+ // },
403+
404+ // updateAnnotations: function() {
405+
406+ // },
407+
408+ // getAnnotations: function() {
409+
410+ // },
411+
412+ // removeAnnotations: function() {
413+
414+ // },
415+
416+ // addRelation: function() {
417+
418+ // },
419+
420+ // removeRelation: function() {
421+
422+ // },
423+
424+ // expose: function() {
425+
426+ // },
427+
428+ // unexpose: function() {
429+
430+ // },
431+
432+ // setConfig: function() {
433+
434+ // },
435+
436+ // setConstraints: function() {
437+
438+ // },
439+
440+ // resolved: function() {
441+
442+ // }
443+
444+ });
445+
446+ Y.namespace('juju.environments').FakeBackend = FakeBackend;
447+
448+}, '0.1.0', {
449+ requires: [
450+ 'base',
451+ 'juju-models'
452+ ]
453+});
454
455=== added file 'test/data/wordpress-charmdata.json'
456--- test/data/wordpress-charmdata.json 1970-01-01 00:00:00 +0000
457+++ test/data/wordpress-charmdata.json 2013-03-11 15:03:24 +0000
458@@ -0,0 +1,63 @@
459+{
460+ "maintainer": "Marco Ceppi <marco@ceppi.net>",
461+ "owner": "charmers",
462+ "series": "precise",
463+ "provides": {
464+ "website": {
465+ "interface": "http"
466+ }
467+ },
468+ "config": {
469+ "options": {
470+ "debug": {
471+ "default": "no",
472+ "type": "string",
473+ "description": "Setting this option to \"yes\" will expose /_debug on all instances over HTTP. In the _debug folder are two scripts, info.php and apc.php. info.php will display the phpinfo information for that server while the apc.php will provide APC cache stats (as well as additional administrative options for APC).\n"
474+ },
475+ "engine": {
476+ "default": "nginx",
477+ "type": "string",
478+ "description": "Currently two web server engines are supported: nginx and apache. For the majority of deployments nginx will be the prefered choice. See the Readme for more details"
479+ },
480+ "tuning": {
481+ "default": "single",
482+ "type": "string",
483+ "description": "This is the tuning level for the WordPress setup. There are three options: \"bare\", \"single\", and \"optimized\". \"bare\" will give you a nearly un-altered WordPress setup, as if you'd downloaded and set it up yourself. \"single\" will provide you with everything you need to run a singlular unit of WordPress. This doesn't take in to consideration that you'll be scaling at all. However, it will allow you to use WordPress free of any troubles and pesky limitations that typically happen during \"optimized\". While you _can_ scale out with this setting I encourage you read the README \"optimized\" will give you a hardened WordPress setup. Some of the features in the Admin panel will be locked down and theme edits/plugins can only be updated through he charm. This is the recommended setup for those who are in serious need of constant scaling. \n"
484+ },
485+ "wp-content": {
486+ "default": "",
487+ "type": "string",
488+ "description": "This is a full repository path to where the WordPress wp-contents can be found. At this time Git, BZR, SVN, and HG are supported. An example of what a wp-content repository should look like can be found at http://github.com/jujutools/wordpress-site.\n"
489+ }
490+ }
491+ },
492+ "description": "This will install and setup WordPress optimized to run in the cloud. This install, in particular, will \nplace Ngnix and php-fpm configured to scale horizontally with Nginx's reverse proxy\n",
493+ "store_url": "cs:precise/wordpress-10",
494+ "last_change": {
495+ "committer": "Marco Ceppi <marco@ceppi.net>",
496+ "message": "# Changes\n\n* Add Apache2 support via engine config\n* Add VCS bash charm helper for wp-content\n* Fix defaults in configuration\n* Fix secret key scale-out\n* Add copyright headers to files\n\n# Commits\n\n Marco Ceppi 2012-12-13 Fix for secrety key and cookies\n Marco Ceppi 2012-12-13 Make sure apache respects the canonical name for cookies and the like\n Marco Ceppi 2012-12-13 Preserve host\n Marco Ceppi 2012-12-13 Set the request header to make sure assets work properly\n Marco Ceppi 2012-12-13 [merge] proxy_http\n Marco Ceppi 2012-12-12 proxy_http needs to be enabled\n Marco Ceppi 2012-12-12 Include proxy_http\n Marco Ceppi 2012-12-12 Enable proxy_balancer\n Marco Ceppi 2012-12-12 Fix to balancermember lines and conditional for nginx only memcached configuration\n Marco Ceppi 2012-12-12 Fix to loadbalancer conf name for apache2\n Marco Ceppi 2012-12-11 Server lines need a ';' and when loading from cache need to strip() results\n Marco Ceppi 2012-12-11 Use the restart hook\n Marco Ceppi 2012-12-11 Backwards compat fix\n Marco Ceppi 2012-12-11 Enable multiverse for the apache2 stuff\n Marco Ceppi 2012-12-11 Make sure sed expands shell variable\n Marco Ceppi 2012-12-11 Don't need to listen on 80\n Marco Ceppi 2012-12-11 Final changes to make apache2 switches work properly in the charm\n Marco Ceppi 2012-12-11 Added default apache files\n Marco Ceppi 2012-12-11 Updates to fix linting issues, added Apache template\n Marco Ceppi 2012-12-11 Changes to website relation\n Marco Ceppi 2012-11-21 Updated files with copyright headers, adding in all apache stuff, new loadbalancer-rebuild sym, apache files, etc\n Marco Ceppi 2012-11-21 Now we use charm-helper\n",
497+ "revno": 61,
498+ "created": 1355429822.709
499+ },
500+ "store_revision": 10,
501+ "peers": {
502+ "loadbalancer": {
503+ "interface": "reversenginx"
504+ }
505+ },
506+ "name": "wordpress",
507+ "summary": "WordPress is a full featured web blogging tool, this charm deploys it.",
508+ "bzr_branch": "lp:~charmers/charms/precise/wordpress/trunk",
509+ "requires": {
510+ "nfs": {
511+ "interface": "mount"
512+ },
513+ "cache": {
514+ "interface": "memcache"
515+ },
516+ "db": {
517+ "interface": "mysql"
518+ }
519+ },
520+ "proof": {}
521+}
522\ No newline at end of file
523
524=== modified file 'test/index.html'
525--- test/index.html 2013-03-07 22:40:42 +0000
526+++ test/index.html 2013-03-11 15:03:24 +0000
527@@ -49,6 +49,7 @@
528 <script src="test_env_go.js"></script>
529 <script src="test_env_python.js"></script>
530 <script src="test_environment_view.js"></script>
531+ <script src="test_fakebackend.js"></script>
532 <script src="test_landscape.js"></script>
533 <script src="test_login.js"></script>
534 <script src="test_model.js"></script>
535
536=== added file 'test/test_fakebackend.js'
537--- test/test_fakebackend.js 1970-01-01 00:00:00 +0000
538+++ test/test_fakebackend.js 2013-03-11 15:03:24 +0000
539@@ -0,0 +1,292 @@
540+'use strict';
541+
542+(function() {
543+
544+ describe('FakeBackend.login', function() {
545+ var requires = ['node', 'juju-env-fakebackend'];
546+ var Y, environmentsModule, fakebackend;
547+
548+ before(function(done) {
549+ Y = YUI(GlobalConfig).use(requires, function(Y) {
550+ environmentsModule = Y.namespace('juju.environments');
551+ done();
552+ });
553+ });
554+
555+ afterEach(function() {
556+ fakebackend.destroy();
557+ });
558+
559+ it('authenticates', function() {
560+ fakebackend = new environmentsModule.FakeBackend();
561+ assert.equal(fakebackend.get('authenticated'), false);
562+ assert.equal(fakebackend.login('admin', 'password'), true);
563+ assert.equal(fakebackend.get('authenticated'), true);
564+ });
565+
566+ it('refuses to authenticate', function() {
567+ fakebackend = new environmentsModule.FakeBackend();
568+ assert.equal(fakebackend.get('authenticated'), false);
569+ assert.equal(fakebackend.login('admin', 'not my password'), false);
570+ assert.equal(fakebackend.get('authenticated'), false);
571+ });
572+ });
573+
574+ var makeFakeBackendWithCharmStore = function(Y, juju, module) {
575+ var data = [];
576+ var charmStore = new juju.CharmStore(
577+ {datasource: new Y.DataSource.Local({source: data})});
578+ var setCharm = function(name) {
579+ data[0] = (
580+ { responseText: Y.io(
581+ 'data/' + name + '-charmdata.json', {sync: true}).responseText });
582+ };
583+ setCharm('wordpress');
584+ var fakebackend = new module.FakeBackend(
585+ {charmStore: charmStore});
586+ fakebackend.login('admin', 'password');
587+ return {fakebackend: fakebackend, setCharm: setCharm};
588+ };
589+
590+ describe('FakeBackend.deploy', function() {
591+ var requires = [
592+ 'node', 'juju-env-fakebackend', 'datasource-local', 'io',
593+ 'juju-charm-store', 'juju-models', 'juju-charm-models'];
594+ var Y, fakebackend, environmentsModule, setCharm, juju, result, callback;
595+
596+ before(function(done) {
597+ Y = YUI(GlobalConfig).use(requires, function(Y) {
598+ environmentsModule = Y.namespace('juju.environments');
599+ juju = Y.namespace('juju');
600+ done();
601+ });
602+ });
603+
604+ beforeEach(function() {
605+ var setupData = makeFakeBackendWithCharmStore(
606+ Y, juju, environmentsModule);
607+ fakebackend = setupData.fakebackend;
608+ setCharm = setupData.setCharm;
609+ result = undefined;
610+ callback = function(response) { result = response; };
611+ });
612+
613+ afterEach(function() {
614+ fakebackend.destroy();
615+ });
616+
617+ it('rejects unauthenticated calls', function() {
618+ fakebackend.logout();
619+ fakebackend.deploy('cs:wordpress', callback);
620+ assert.equal(result.error, 'Please log in.');
621+ });
622+
623+ it('rejects poorly formed charm ids', function() {
624+ fakebackend.deploy('shazam!!!!!!', callback);
625+ assert.equal(result.error, 'Invalid charm id.');
626+ });
627+
628+ it('deploys a charm', function() {
629+ // Defaults service name to charm name; defaults unit count to 1.
630+ assert.isNull(
631+ fakebackend.db.charms.getById('cs:precise/wordpress-10'));
632+ assert.isUndefined(fakebackend.deploy('cs:wordpress', callback));
633+ assert.isUndefined(result.error);
634+ assert.isObject(
635+ fakebackend.db.charms.getById('cs:precise/wordpress-10'));
636+ var service = fakebackend.db.services.getById('wordpress');
637+ assert.isObject(service);
638+ assert.strictEqual(service, result.service);
639+ var attrs = service.getAttrs();
640+ // clientId varies.
641+ assert.isTrue(Y.Lang.isString(attrs.clientId));
642+ delete attrs.clientId;
643+ assert.deepEqual(attrs, {
644+ charm: 'cs:precise/wordpress-10',
645+ config: undefined,
646+ destroyed: false,
647+ exposed: false,
648+ id: 'wordpress',
649+ initialized: true,
650+ name: 'wordpress',
651+ subordinate: undefined
652+ });
653+ var units = fakebackend.db.units.get_units_for_service(service);
654+ assert.lengthOf(units, 1);
655+ assert.lengthOf(result.units, 1);
656+ assert.strictEqual(units[0], result.units[0]);
657+ assert.equal(units[0].service, 'wordpress');
658+ });
659+
660+ it('rejects names that duplicate an existing service', function() {
661+ fakebackend.deploy('cs:wordpress', callback);
662+ assert.isUndefined(result.error);
663+ // The service name is provided explicitly.
664+ fakebackend.deploy('cs:haproxy', callback, {name: 'wordpress'});
665+ assert.equal(result.error, 'A service with this name already exists.');
666+ // The service name is derived from charm.
667+ result = undefined;
668+ fakebackend.deploy('cs:wordpress', callback);
669+ assert.equal(result.error, 'A service with this name already exists.');
670+ });
671+
672+ it('reuses already-loaded charms with the same explicit id.', function() {
673+ fakebackend._loadCharm('cs:wordpress');
674+ assert.isObject(
675+ fakebackend.db.charms.getById('cs:precise/wordpress-10'));
676+ // Eliminate the charmStore to show we reuse the pre-loaded charm.
677+ fakebackend.set('charmStore', undefined);
678+ fakebackend.deploy('cs:precise/wordpress-10', callback);
679+ assert.isUndefined(result.error);
680+ assert.isObject(result.service);
681+ assert.equal(result.service.get('charm'), 'cs:precise/wordpress-10');
682+ });
683+
684+ it('reuses already-loaded charms with the same id.', function() {
685+ fakebackend._loadCharm('cs:wordpress');
686+ var charm = fakebackend.db.charms.getById('cs:precise/wordpress-10');
687+ assert.equal(fakebackend.db.charms.size(), 1);
688+ // The charm data shows that this is not a subordinate charm. We will
689+ // change this in the db, to show that the db data is used within the
690+ // deploy code.
691+ assert.isUndefined(charm.get('is_subordinate'));
692+ // The _set forces a change to a writeOnly attribute.
693+ charm._set('is_subordinate', true);
694+ fakebackend.deploy('cs:wordpress', callback);
695+ assert.isUndefined(result.error);
696+ assert.strictEqual(
697+ fakebackend.db.charms.getById('cs:precise/wordpress-10'), charm);
698+ assert.equal(fakebackend.db.charms.size(), 1);
699+ assert.equal(result.service.get('charm'), 'cs:precise/wordpress-10');
700+ // This is the clearest indication that we used the db version, as
701+ // opposed to the charmStore version, per the comments above.
702+ assert.isTrue(result.service.get('subordinate'));
703+ });
704+
705+ it('accepts a config.', function() {
706+ fakebackend.deploy(
707+ 'cs:wordpress', callback, {config: {funny: 'business'}});
708+ assert.deepEqual(result.service.get('config'), {funny: 'business'});
709+ });
710+
711+ it('deploys multiple units.', function() {
712+ fakebackend.deploy('cs:wordpress', callback, {unitCount: 3});
713+ var units = fakebackend.db.units.get_units_for_service(result.service);
714+ assert.lengthOf(units, 3);
715+ assert.lengthOf(result.units, 3);
716+ assert.deepEqual(units, result.units);
717+ });
718+
719+ it('reports when the charm store is inaccessible.', function() {
720+ fakebackend.get('charmStore').loadByPath = function(path, options) {
721+ options.failure({boo: 'hiss'});
722+ };
723+ fakebackend.deploy('cs:wordpress', callback);
724+ assert.equal(result.error, 'Could not contact charm store.');
725+ });
726+
727+ it('honors the optional service name', function() {
728+ assert.isUndefined(
729+ fakebackend.deploy('cs:wordpress', callback, {name: 'kumquat'}));
730+ assert.equal(result.service.get('id'), 'kumquat');
731+ });
732+
733+ // it('accepts a config file.');
734+
735+ // it('does not accept both a config and config file.');
736+
737+ // it('records when services and units are added.');
738+
739+ });
740+
741+ describe('FakeBackend.addUnit', function() {
742+ var requires = [
743+ 'node', 'juju-env-fakebackend', 'datasource-local', 'io',
744+ 'juju-charm-store', 'juju-models', 'juju-charm-models'];
745+ var Y, fakebackend, environmentsModule, setCharm, juju,
746+ deployResult, callback;
747+
748+ before(function(done) {
749+ Y = YUI(GlobalConfig).use(requires, function(Y) {
750+ environmentsModule = Y.namespace('juju.environments');
751+ juju = Y.namespace('juju');
752+ done();
753+ });
754+ });
755+
756+ beforeEach(function() {
757+ var setupData = makeFakeBackendWithCharmStore(
758+ Y, juju, environmentsModule);
759+ fakebackend = setupData.fakebackend;
760+ setCharm = setupData.setCharm;
761+ deployResult = undefined;
762+ callback = function(response) { deployResult = response; };
763+ });
764+
765+ afterEach(function() {
766+ fakebackend.destroy();
767+ });
768+
769+ it('rejects unauthenticated calls', function() {
770+ fakebackend.logout();
771+ var result = fakebackend.addUnit('wordpress');
772+ assert.equal(result.error, 'Please log in.');
773+ });
774+
775+ it('returns an error for an invalid number of units', function() {
776+ fakebackend.deploy('cs:wordpress', callback);
777+ assert.isUndefined(deployResult.error);
778+ assert.equal(
779+ fakebackend.addUnit('wordpress', 'goyesca').error,
780+ 'Invalid number of units.');
781+ assert.equal(
782+ fakebackend.addUnit('wordpress', 0).error,
783+ 'Invalid number of units.');
784+ assert.equal(
785+ fakebackend.addUnit('wordpress', -1).error,
786+ 'Invalid number of units.');
787+ });
788+
789+ it('returns an error if the service does not exist.', function() {
790+ assert.equal(
791+ fakebackend.addUnit('foo').error,
792+ 'Service "foo" does not exist.');
793+ });
794+
795+ it('defaults to adding just one unit', function() {
796+ fakebackend.deploy('cs:wordpress', callback);
797+ assert.isUndefined(deployResult.error);
798+ assert.lengthOf(
799+ fakebackend.db.units.get_units_for_service(deployResult.service), 1);
800+ var result = fakebackend.addUnit('wordpress');
801+ assert.lengthOf(result.units, 1);
802+ assert.lengthOf(
803+ fakebackend.db.units.get_units_for_service(deployResult.service), 2);
804+ // Units are simple objects, not models.
805+ assert.equal(result.units[0].id, 'wordpress/2');
806+ assert.equal(result.units[0].agent_state, 'started');
807+ assert.deepEqual(
808+ result.units[0], fakebackend.db.units.getById('wordpress/2'));
809+ // TODO Verify that machines exist.
810+ });
811+
812+ it('adds multiple units', function() {
813+ fakebackend.deploy('cs:wordpress', callback);
814+ assert.isUndefined(deployResult.error);
815+ assert.lengthOf(
816+ fakebackend.db.units.get_units_for_service(deployResult.service), 1);
817+ var result = fakebackend.addUnit('wordpress', 5);
818+ assert.lengthOf(result.units, 5);
819+ assert.lengthOf(
820+ fakebackend.db.units.get_units_for_service(deployResult.service), 6);
821+ assert.equal(result.units[0].id, 'wordpress/2');
822+ assert.equal(result.units[1].id, 'wordpress/3');
823+ assert.equal(result.units[2].id, 'wordpress/4');
824+ assert.equal(result.units[3].id, 'wordpress/5');
825+ assert.equal(result.units[4].id, 'wordpress/6');
826+ });
827+
828+ // it('records when services and units are added.');
829+
830+ });
831+})();
832
833=== modified file 'test/test_service_view.js'
834--- test/test_service_view.js 2013-02-27 22:25:50 +0000
835+++ test/test_service_view.js 2013-03-11 15:03:24 +0000
836@@ -82,7 +82,8 @@
837 });
838
839 it('should not show controls if the charm is subordinate', function() {
840- charm.set('is_subordinate', true);
841+ // The _set forces a change to a writeOnly attribute.
842+ charm._set('is_subordinate', true);
843 var view = makeServiceView();
844 // "var _ =" makes the linter happy.
845 var _ = expect(container.one('#num-service-units')).to.not.exist;

Subscribers

People subscribed via source and target branches