Merge lp:~gary/juju-gui/fakebackend-1 into lp:juju-gui/experimental
- fakebackend-1
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+152696@code.launchpad.net |
Commit message
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.
Gary Poster (gary) wrote : | # |
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:/
File app/models/charm.js (right):
https:/
app/models/
Nice cleanup
https:/
File app/store/
https:/
app/store/
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:/
File test/test_
https:/
test/test_
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.
}
sample_env = loadFixture(
I think its better to reuse this even if its a line or two.
Madison Scott-Clary (makyo) wrote : | # |
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:/
> File app/models/charm.js (right):
https:/
> app/models/
> Nice cleanup
Thanks.
https:/
> File app/store/
https:/
> app/store/
> 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:/
> File test/test_
https:/
> test/test_
{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.
> }
> sample_env = loadFixture(
> sample_endpoints = loadFixture(
> 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
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:/
Preview Diff
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; |
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: debug.js env/fakebackend .js wordpress- charmdata. json fakebackend. js service_ view.js
A .lbox
A [revision details]
M app/models/charm.js
M app/modules-
A app/store/
A test/data/
M test/index.html
A test/test_
M test/test_