Merge lp:~hatch/juju-gui/save-changed-1214087 into lp:juju-gui/experimental

Proposed by Jeff Pihach
Status: Merged
Merged at revision: 1000
Proposed branch: lp:~hatch/juju-gui/save-changed-1214087
Merge into: lp:juju-gui/experimental
Diff against target: 826 lines (+268/-67)
17 files modified
app/store/env/fakebackend.js (+28/-2)
app/store/env/go.js (+51/-6)
app/store/env/python.js (+18/-5)
app/views/ghost-inspector.js (+2/-0)
app/views/inspector.js (+2/-0)
app/views/service.js (+1/-0)
app/views/utils.js (+38/-2)
app/views/viewlets/service-ghost.js (+5/-0)
test/assets/mysql-config.yaml (+1/-1)
test/test_env_go.js (+9/-2)
test/test_env_python.js (+9/-10)
test/test_fakebackend.js (+21/-6)
test/test_ghost_inspector.js (+3/-7)
test/test_sandbox_go.js (+50/-12)
test/test_sandbox_python.js (+22/-7)
test/test_service_config_view.js (+4/-3)
test/test_utils.js (+4/-4)
To merge this branch: bzr merge lp:~hatch/juju-gui/save-changed-1214087
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+183048@code.launchpad.net

Description of the change

Only sends the changed config options.

1) There was a bug with the ghost constraints code which
   caused it to fail when deploying services on rapi.

2) The GUI now only sends configuration values to juju when
   those values differ from the defaults.

https://codereview.appspot.com/13252045/

To post a comment you must log in.
Revision history for this message
Jeff Pihach (hatch) wrote :
Download full text (5.6 KiB)

Reviewers: mp+183048_code.launchpad.net,

Message:
Please take a look.

Description:
Only sends the changed config options.

1) There was a bug with the ghost constraints code which
    caused it to fail when deploying services on rapi.

2) The GUI now only sends configuration values to juju when
    those values differ from the defaults.

To QA: Deploy charms on sandbox, rapi, juju-core. Modifying and
        not modifying the default constraints and configuration.
        Then open the inspector and check to make sure the settings
        are appropriate and that the constraints page shows the
        proper constraints.

https://code.launchpad.net/~hatch/juju-gui/save-changed-1214087/+merge/183048

(do not edit description out of merge proposal)

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

Affected files:
   A [revision details]
   M app/views/ghost-inspector.js
   M app/views/inspector.js
   M app/views/viewlets/service-ghost.js
   M test/test_ghost_inspector.js

Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: <email address hidden>
+New revision: <email address hidden>

Index: test/test_ghost_inspector.js
=== modified file 'test/test_ghost_inspector.js'
--- test/test_ghost_inspector.js 2013-08-27 16:30:39 +0000
+++ test/test_ghost_inspector.js 2013-08-29 20:39:26 +0000
@@ -139,11 +139,7 @@
      var message = env.ws.last_message();
      var params = message.Params;
      var config = {
- admins: '',
- debug: 'false',
- logo: '',
- name: 'foo',
- skin: 'vector'
+ name: 'foo'
      };
      assert.equal('ServiceDeploy', message.Request);
      assert.equal('mediawiki', params.ServiceName);
@@ -189,7 +185,7 @@
      vmContainer.one('.viewlet-manager-footer
button.confirm').simulate('click');

      var message = env.ws.last_message();
- assert.equal(message.constraints.cpu, '2');
+ assert.deepEqual(message.constraints, ['cpu=2', 'mem=', 'arch=']);
    });

    it('deploys with constraints in go env', function() {
@@ -204,7 +200,8 @@
      vmContainer.one('.viewlet-manager-footer
button.confirm').simulate('click');

      var message = env.ws.last_message();
- assert.equal(message.Params.Constraints['cpu-power'], '2');
+ assert.deepEqual(message.Params.Constraints,
+ ['cpu-power=2', 'cpu-cores=', 'mem=', 'arch=']);
    });

    it('disables and resets input fields when \'use default config\' is
active',

Index: app/views/ghost-inspector.js
=== modified file 'app/views/ghost-inspector.js'
--- app/views/ghost-inspector.js 2013-08-26 14:16:34 +0000
+++ app/views/ghost-inspector.js 2013-08-29 20:39:26 +0000
@@ -121,9 +121,30 @@
              container, '.service-config .config-field');
        }

+ var charmOptions = options.model.get('options');
+ /*jshint -W089 */
+ // Tells jshint to ignore the lack of hasOwnProperty in forloops
+ for (var cfgOption in config) {
+ // Remove config options which are not different from the charm ...

Read more...

Revision history for this message
Francesco Banconi (frankban) wrote :

Thanks for this branch Jeff. I think there is still some substantial
work/clean up to do here (see the comments below). The constraints
problem prevented me from deploying charms in juju-core most of the
times. In the go sandbox, I was able to deploy services and set
constraints, but a further look in the inspector showed the constraints
to be broken (e.g. a constraint named "1" with value "cpu-cores=4"). I
believe this is the same problem described below.

https://codereview.appspot.com/13252045/diff/1/app/views/ghost-inspector.js
File app/views/ghost-inspector.js (right):

https://codereview.appspot.com/13252045/diff/1/app/views/ghost-inspector.js#newcode141
app/views/ghost-inspector.js:141: // Format the constraints into the
proper format.
This is not the proper format for juju-core, e.g.:

D 130830 08:33:57 handlers:148] GET /ws (10.0.3.1) client -> juju:
{"Type":"Client","Request":"ServiceDeploy","Params":{"ServiceName":"mediawiki","Config":{},"Constraints":["cpu-power=","cpu-cores=4","mem=1","arch="],"CharmUrl":"cs:precise/mediawiki-9","NumUnits":1},"RequestId":17}
[D 130830 08:33:57 handlers:164] GET /ws (10.0.3.1) juju -> client:
{"RequestId":17,"Error":"json: cannot unmarshal array into Go value of
type constraints.Value","Response":{}}

The deploy method of the GoEnvironment, and the set_constraints as well,
requires an object to be passed. The juju-core constraints.Value itself
is a struct, not a slice. The deploy method of the PyEnvironment does
not mention "constraints" in its docstring (and this should be fixed).
Since we eventually want to stop maintaining the pyjuju backend, I'd
suggest to generate the constraints as an object (like it is currently
done), and make the proper conversions only ub the case the environment
is python (or the python sandbox).
Does this make sense?

https://codereview.appspot.com/13252045/diff/1/app/views/inspector.js
File app/views/inspector.js (right):

https://codereview.appspot.com/13252045/diff/1/app/views/inspector.js#newcode739
app/views/inspector.js:739: /*jshint -W089 */
This logic is used by both inspectors: I'd like to see this code
abstracted in a separate function, reused when required, and tested. A
comment explaining why we need implicit type conversion would be great.

https://codereview.appspot.com/13252045/

997. By Jeff Pihach

fixed difference between backend constraint formats

Revision history for this message
Jeff Pihach (hatch) wrote :
998. By Jeff Pihach

testing for go backend push

999. By Jeff Pihach

fixed constraints parsing

1000. By Jeff Pihach

tests and lint

Revision history for this message
Jeff Pihach (hatch) wrote :
Revision history for this message
Richard Harding (rharding) wrote :

I think this moves the environment specific logic up to the view/app
layer. The inspector or other views shouldn't have to worry about
transposing the constraints. I think that should be moved down into the
environment and the utils helper made part of the env as well so that
the logic is localized since the env tests are what's testing the code
works anyway.

https://codereview.appspot.com/13252045/diff/11001/app/store/env/go.js
File app/store/env/go.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/store/env/go.js#newcode130
app/store/env/go.js:130: genericConstraints: ['cpu-power', 'cpu-cores',
'mem', 'arch'],
why not just set these as an object with key name, { type: } ?

https://codereview.appspot.com/13252045/diff/11001/app/views/ghost-inspector.js
File app/views/ghost-inspector.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/views/ghost-inspector.js#newcode130
app/views/ghost-inspector.js:130: // PyJuju does not rquire some
constraints to be
r*e*require

https://codereview.appspot.com/13252045/diff/11001/app/views/ghost-inspector.js#newcode132
app/views/ghost-inspector.js:132: var integerConstraints =
this.options.env.integerConstraints;
should the env.deploy function do the conversion instead of the
ghost-inspector? What if this logic were used elsewhere. If it's always
in the deploy then it won't need to be repeated.

https://codereview.appspot.com/13252045/diff/11001/app/views/inspector.js
File app/views/inspector.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/views/inspector.js#newcode741
app/views/inspector.js:741: env.set_config(
again, can set_config do the check for unchanged options? It seems it
would know the old values and have a list of the new ones to do the work
in the back end so that the inspector doesn't have to worry about back
end implementation.

https://codereview.appspot.com/13252045/diff/11001/app/views/utils.js
File app/views/utils.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/views/utils.js#newcode670
app/views/utils.js:670: utils.removeUnchangedConfigOptions =
function(config, charmOptions) {
This could be moved to the env itself as a helper used in there.

https://codereview.appspot.com/13252045/diff/11001/test/test_env_python.js
File test/test_env_python.js (right):

https://codereview.appspot.com/13252045/diff/11001/test/test_env_python.js#newcode74
test/test_env_python.js:74: var constraints = { cpu: '1', mem: '512M',
arch: 'i386'};
?? did this cause a lint issue or something?

https://codereview.appspot.com/13252045/

Revision history for this message
Jeff Pihach (hatch) wrote :

Thanks for the review - comments below and changes incoming.

I agree with your comments but where noted we can discuss in more detail
tomorrow.

https://codereview.appspot.com/13252045/diff/11001/app/store/env/go.js
File app/store/env/go.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/store/env/go.js#newcode130
app/store/env/go.js:130: genericConstraints: ['cpu-power', 'cpu-cores',
'mem', 'arch'],
I didn't want to change everywhere else in the application that accessed
the constraints objects from either back end and all of their associated
tests.

https://codereview.appspot.com/13252045/diff/11001/app/views/ghost-inspector.js
File app/views/ghost-inspector.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/views/ghost-inspector.js#newcode130
app/views/ghost-inspector.js:130: // PyJuju does not rquire some
constraints to be
On 2013/09/02 21:19:28, rharding wrote:
> r*e*require

Done.

https://codereview.appspot.com/13252045/diff/11001/app/views/ghost-inspector.js#newcode132
app/views/ghost-inspector.js:132: var integerConstraints =
this.options.env.integerConstraints;
That's a good idea, done.

https://codereview.appspot.com/13252045/diff/11001/app/views/inspector.js
File app/views/inspector.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/views/inspector.js#newcode741
app/views/inspector.js:741: env.set_config(
Lets discuss this tomorrow - I figured that this method may be called
directly and not have it's values run through this method.

https://codereview.appspot.com/13252045/diff/11001/app/views/utils.js
File app/views/utils.js (right):

https://codereview.appspot.com/13252045/diff/11001/app/views/utils.js#newcode670
app/views/utils.js:670: utils.removeUnchangedConfigOptions =
function(config, charmOptions) {
As previously mentioned - lets discuss tomorrow.

https://codereview.appspot.com/13252045/diff/11001/test/test_env_python.js
File test/test_env_python.js (right):

https://codereview.appspot.com/13252045/diff/11001/test/test_env_python.js#newcode74
test/test_env_python.js:74: var constraints = { cpu: '1', mem: '512M',
arch: 'i386'};
I don't think so, probably just an old habit.

https://codereview.appspot.com/13252045/

1001. By Jeff Pihach

minor refactoring from review

1002. By Jeff Pihach

merged trunk

Revision history for this message
Jeff Pihach (hatch) wrote :
1003. By Jeff Pihach

refactored set config to only send changed values

Revision history for this message
Jeff Pihach (hatch) wrote :
Revision history for this message
Richard Harding (rharding) wrote :

LGTM thanks for getting this put together!

https://codereview.appspot.com/13252045/

1004. By Jeff Pihach

normalized the constraint filtering in the go backend

1005. By Jeff Pihach

lint

Revision history for this message
Jeff Pihach (hatch) wrote :
1006. By Jeff Pihach

fix incorrect typeof bug

Revision history for this message
Jeff Pihach (hatch) wrote :
Revision history for this message
Francesco Banconi (frankban) wrote :

LGTM QA ok, thank you Jeff

https://codereview.appspot.com/13252045/

Revision history for this message
Jeff Pihach (hatch) wrote :

*** Submitted:

Only sends the changed config options.

1) There was a bug with the ghost constraints code which
    caused it to fail when deploying services on rapi.

2) The GUI now only sends configuration values to juju when
    those values differ from the defaults.

R=frankban, rharding
CC=
https://codereview.appspot.com/13252045

https://codereview.appspot.com/13252045/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/store/env/fakebackend.js'
2--- app/store/env/fakebackend.js 2013-09-03 10:02:52 +0000
3+++ app/store/env/fakebackend.js 2013-09-03 15:47:40 +0000
4@@ -374,14 +374,40 @@
5 constraints = options.constraints;
6 }
7
8+ // In order for the constraints to support the python back end this
9+ // needs to be an array, so we are converting it back to an object
10+ // here so that the GUI displays it properly.
11+ var constraintsMap = {}, vals;
12+ if (Y.Lang.isArray(constraints)) {
13+ constraints.forEach(function(cons) {
14+ vals = cons.split('=');
15+ constraintsMap[vals[0]] = vals[1];
16+ });
17+ } else {
18+ constraintsMap = constraints;
19+ }
20+
21 var service = this.db.services.add({
22 id: options.name,
23 name: options.name,
24 charm: charm.get('id'),
25- constraints: constraints,
26+ constraints: constraintsMap,
27 exposed: false,
28 subordinate: charm.get('is_subordinate'),
29- config: options.config
30+ // Because we only send the user changed options now
31+ // we need to mix those values in to the charm config
32+ // options when creating a new model.
33+ config: (function() {
34+ var charmOptions = charm.get('options');
35+ var config = {};
36+ if (!options.config) { options.config = {}; }
37+ Object.keys(charmOptions).forEach(function(key) {
38+ config[key] =
39+ options.config[key] ||
40+ (charmOptions[key] ? charmOptions[key].default : undefined);
41+ });
42+ return config;
43+ })()
44 });
45 this.changes.services[service.get('id')] = [service, true];
46 var response = this.addUnit(options.name, options.unitCount);
47
48=== modified file 'app/store/env/go.js'
49--- app/store/env/go.js 2013-08-28 19:21:23 +0000
50+++ app/store/env/go.js 2013-09-03 15:47:40 +0000
51@@ -28,6 +28,7 @@
52 YUI.add('juju-env-go', function(Y) {
53
54 var environments = Y.namespace('juju.environments');
55+ var utils = Y.namespace('juju.views.utils');
56
57 var endpointToName = function(endpoint) {
58 return endpoint[0] + ':' + endpoint[1].name;
59@@ -119,9 +120,28 @@
60
61 Y.extend(GoEnvironment, environments.BaseEnvironment, {
62
63+ /**
64+ A list of the valid constraints for all providers. Required
65+ because we cannot request these constraints from Juju yet.
66+
67+ @property genericConstraints
68+ @default ['cpu-power', 'cpu-cores', 'mem', 'arch']
69+ @type {Array}
70+ */
71 genericConstraints: ['cpu-power', 'cpu-cores', 'mem', 'arch'],
72
73 /**
74+ A list of the constraints that need to be integers. We require
75+ this list because we cannot request the valid constraints from
76+ Juju.
77+
78+ @property integerConstraints
79+ @default ['cpu-power', 'cpu-cores', 'mem']
80+ @type {Array}
81+ */
82+ integerConstraints: ['cpu-power', 'cpu-cores', 'mem'],
83+
84+ /**
85 * Go environment constructor.
86 *
87 * @method initializer
88@@ -264,6 +284,30 @@
89 },
90
91 /**
92+ Utility method for filtering the constraint data and removing constraints
93+ which do not have valid values.
94+
95+ @method filterConstraints
96+ @param {Object} constraints key:value pairs.
97+ @return an object of valid constraint values.
98+ */
99+ filterConstraints: function(constraints) {
100+ // Some of the constraints have to be integers.
101+ this.integerConstraints.forEach(function(key) {
102+ constraints[key] = parseInt(constraints[key], 10) || undefined;
103+ });
104+ // Remove all constraint options which don't have values
105+ // else the go back end has issues.
106+ Object.keys(constraints).forEach(function(key) {
107+ if (constraints[key] === undefined ||
108+ (constraints[key].trim && constraints[key].trim() === '')) {
109+ delete constraints[key];
110+ }
111+ });
112+ return constraints;
113+ },
114+
115+ /**
116 * React to the results of sending a login message to the server.
117 *
118 * @method handleLogin
119@@ -383,6 +427,8 @@
120 console.error('Constraints need to be an object not a function');
121 console.warn(constraints);
122 }
123+
124+ constraints = this.filterConstraints(constraints);
125 } else {
126 constraints = {};
127 }
128@@ -852,14 +898,15 @@
129 @param {String} data The YAML representation of the charm
130 configuration options. Only one of `config` and `data` should be
131 provided, though `data` takes precedence if it is given.
132+ @param {Object} serviceConfig the current configuration object
133+ of the service.
134 @param {Function} callback A callable that must be called once the
135 operation is performed. It will receive an object containing:
136 err - a string describing the problem (if an error occurred),
137 service_name - the name of the service.
138 @return {undefined} Sends a message to the server only.
139 */
140- set_config: function(serviceName, config, data, callback) {
141-
142+ set_config: function(serviceName, config, data, serviceConfig, callback) {
143 if ((Y.Lang.isValue(config) && Y.Lang.isValue(data)) ||
144 (!Y.Lang.isValue(config) && !Y.Lang.isValue(data))) {
145 throw 'Exactly one of config and data must be provided';
146@@ -878,6 +925,7 @@
147 sendData.Request = 'ServiceSetYAML';
148 sendData.Params.ConfigYAML = data;
149 } else {
150+ config = utils.removeUnchangedConfigOptions(config, serviceConfig);
151 sendData.Request = 'ServiceSet';
152 sendData.Params.Config = stringifyObjectValues(config);
153 }
154@@ -926,10 +974,7 @@
155 intermediateCallback = Y.bind(this.handleSetConstraints, null,
156 callback, serviceName);
157 }
158- // Some of the constraints have to be numbers.
159- Y.Array.each(['cpu-cores', 'cpu-power', 'mem'], function(key) {
160- constraints[key] = parseInt(constraints[key], 10) || undefined;
161- });
162+ constraints = this.filterConstraints(constraints);
163 sendData = {
164 Type: 'Client',
165 Request: 'SetServiceConstraints',
166
167=== modified file 'app/store/env/python.js'
168--- app/store/env/python.js 2013-08-26 16:52:38 +0000
169+++ app/store/env/python.js 2013-09-03 15:47:40 +0000
170@@ -28,6 +28,7 @@
171 YUI.add('juju-env-python', function(Y) {
172
173 var environments = Y.namespace('juju.environments');
174+ var utils = Y.namespace('juju.views.utils');
175
176 var endpointToName = function(endpoint) {
177 return endpoint[0] + ':' + endpoint[1].name;
178@@ -250,14 +251,21 @@
179 configuration options. Only one of `config` and `config_raw` should be
180 provided, though `config_raw` takes precedence if it is given.
181 * @param {Integer} num_units The number of units to be deployed.
182+ * @param {Object} constraintMap The constraints object.
183 * @param {Function} callback A callable that must be called once the
184 operation is performed.
185 * @return {undefined} Sends a message to the server only.
186 */
187 deploy: function(charm_url, service_name, config, config_raw, num_units,
188- constraints, callback) {
189- if (!constraints) {
190- constraints = {};
191+ constraintMap, callback) {
192+
193+ if (!constraintMap) { constraintMap = {}; }
194+ // Format the constraints properly for the pyjuju backend
195+ var constraints = [];
196+ /* jshint -W089 */
197+ // Tell jshint to ignore the lack of hasOwnProperty in forloops
198+ for (var constr in constraintMap) {
199+ constraints.push(constr + '=' + constraintMap[constr]);
200 }
201
202 this._send_rpc(
203@@ -391,20 +399,25 @@
204 * @param {String} data The YAML representation of the charm
205 configuration options. Only one of `config` and `data` should be
206 provided, though `data` takes precedence if it is given.
207+ * @param {Object} serviceConfig the current configuration object
208+ of the service.
209 * @param {Function} callback A callable that must be called once the
210 operation is performed.
211 * @return {undefined} Sends a message to the server only.
212 */
213- set_config: function(service, config, data, callback) {
214+ set_config: function(service, config, data, serviceConfig, callback) {
215 if ((Y.Lang.isValue(config) && Y.Lang.isValue(data)) ||
216 (!Y.Lang.isValue(config) && !Y.Lang.isValue(data))) {
217 throw 'Exactly one of config and data must be provided';
218 }
219+ config = utils.removeUnchangedConfigOptions(config, serviceConfig);
220+
221 this._send_rpc({
222 op: 'set_config',
223 service_name: service,
224 config: config,
225- data: data}, callback, true);
226+ data: data
227+ }, callback, true);
228 },
229
230 // The constraints that the backend understands. Used to generate forms.
231
232=== modified file 'app/views/ghost-inspector.js'
233--- app/views/ghost-inspector.js 2013-08-26 14:16:34 +0000
234+++ app/views/ghost-inspector.js 2013-09-03 15:47:40 +0000
235@@ -119,6 +119,8 @@
236 } else {
237 config = utils.getElementsValuesMapping(
238 container, '.service-config .config-field');
239+ config = utils.removeUnchangedConfigOptions(
240+ config, options.model.get('options'));
241 }
242
243 // Deploy needs constraints in simple key:value object.
244
245=== modified file 'app/views/inspector.js'
246--- app/views/inspector.js 2013-08-29 17:10:19 +0000
247+++ app/views/inspector.js 2013-09-03 15:47:40 +0000
248@@ -734,11 +734,13 @@
249
250 var newVals = utils.getElementsValuesMapping(container, '.config-field');
251 var errors = utils.validate(newVals, schema);
252+
253 if (Y.Object.isEmpty(errors)) {
254 env.set_config(
255 service.get('id'),
256 newVals,
257 null,
258+ service.get('config'),
259 Y.bind(this._setConfigCallback, this, container)
260 );
261 } else {
262
263=== modified file 'app/views/service.js'
264--- app/views/service.js 2013-08-06 04:35:06 +0000
265+++ app/views/service.js 2013-09-03 15:47:40 +0000
266@@ -630,6 +630,7 @@
267 service.get('id'),
268 new_values,
269 null,
270+ service.get('config'),
271 Y.bind(this._setConfigCallback, this, container)
272 );
273
274
275=== modified file 'app/views/utils.js'
276--- app/views/utils.js 2013-08-30 20:02:41 +0000
277+++ app/views/utils.js 2013-09-03 15:47:40 +0000
278@@ -568,7 +568,7 @@
279 'cpu': {title: 'CPU', unit: 'GHz'},
280 'cpu-cores': {title: 'CPU Cores'},
281 'cpu-power': {title: 'CPU Power', unit: 'GHz'},
282- 'mem': {title: 'Memory', unit: 'GB'}
283+ 'mem': {title: 'Memory', unit: 'MB'}
284 };
285
286 /**
287@@ -659,7 +659,43 @@
288 };
289
290 /**
291- Return a template-friendly array of settings
292+ Removes unchanged config options from a collection of config values and
293+ returns only those that are different from the supplied charm or service
294+ defaults at the time of parsing.
295+
296+ @method removeUnchangedConfigOptions
297+ @param {Object} config is a reference to service config values in the GUI.
298+ @param {Object} options is a reference to the charm or
299+ service configuration options.
300+ @return {Object} An object containing the key/value pairs of config options.
301+ */
302+ utils.removeUnchangedConfigOptions = function(config, options) {
303+ // This method is always called even if the config is provided by
304+ // a configuration file - in this case, return;
305+ if (!config) { return; }
306+ Object.keys(config).forEach(function(key) {
307+ // Remove config options which are not different from the charm defaults
308+ // Intentionally letting the browser do the type coersion.
309+ // The || check is to allow empty inputs to match an undefined default
310+ /* jshint -W116 */
311+ if (Y.Lang.isObject(options[key])) {
312+ // If options is the charm config
313+ if (config[key] == (options[key].default || '')) {
314+ delete config[key];
315+ }
316+ } else {
317+ // If options is a service config
318+ if (config[key] == (options[key] || '')) {
319+ delete config[key];
320+ }
321+ }
322+ /* jshint +W116 */
323+ });
324+ return config;
325+ };
326+
327+ /**
328+ Return a template-friendly array of settings.
329
330 Used for service-configuration.partial adding isBool, isNumeric metadata.
331
332
333=== modified file 'app/views/viewlets/service-ghost.js'
334--- app/views/viewlets/service-ghost.js 2013-08-26 17:03:22 +0000
335+++ app/views/viewlets/service-ghost.js 2013-09-03 15:47:40 +0000
336@@ -41,6 +41,11 @@
337 var newVal = (val['default'] === undefined) ? '' : val['default'];
338 node.set('value', newVal);
339 }
340+ },
341+ 'constraints': {
342+ 'update': function(node, val) {
343+ node.set('value', val || '');
344+ }
345 }
346 },
347 'render': function(model, viewletMgrAttrs) {
348
349=== modified file 'test/assets/mysql-config.yaml'
350--- test/assets/mysql-config.yaml 2012-10-14 16:23:33 +0000
351+++ test/assets/mysql-config.yaml 2013-09-03 15:47:40 +0000
352@@ -1,2 +1,2 @@
353- tuning-level:
354+ tuning:
355 super bad
356
357=== modified file 'test/test_env_go.js'
358--- test/test_env_go.js 2013-08-28 17:15:33 +0000
359+++ test/test_env_go.js 2013-09-03 15:47:40 +0000
360@@ -805,7 +805,14 @@
361 });
362
363 it('can set a service config', function() {
364- env.set_config('mysql', {'cfg-key': 'cfg-val'});
365+ // This also tests that it only sends the changed values
366+ env.set_config('mysql', {
367+ 'cfg-key': 'cfg-val',
368+ 'unchanged': 'bar'
369+ }, null, {
370+ 'cfg-key': 'foo',
371+ 'unchanged': 'bar'
372+ });
373 msg = conn.last_message();
374 var expected = {
375 Type: 'Client',
376@@ -841,7 +848,7 @@
377
378 it('handles failed set config', function() {
379 var err, service_name;
380- env.set_config('yoursql', {}, null, function(evt) {
381+ env.set_config('yoursql', {}, null, {}, function(evt) {
382 err = evt.err;
383 service_name = evt.service_name;
384 });
385
386=== modified file 'test/test_env_python.js'
387--- test/test_env_python.js 2013-08-27 16:24:00 +0000
388+++ test/test_env_python.js 2013-09-03 15:47:40 +0000
389@@ -71,14 +71,12 @@
390 });
391
392 it('successfully deploys a service with constraints', function() {
393- var constraints = {
394- 'cpu': 1,
395- 'mem': '512M',
396- 'arch': 'i386'
397- };
398+ var constraints = { cpu: '1', mem: '512M', arch: 'i386'};
399 env.deploy('precise/mysql', null, null, null, 1, constraints);
400 msg = conn.last_message();
401- assert.deepEqual(msg.constraints, constraints);
402+ // The python backend needs to format them differently than the go
403+ // backend to support rapi.
404+ assert.deepEqual(msg.constraints, [ 'cpu=1', 'mem=512M', 'arch=i386' ]);
405 });
406
407 it('can add a unit', function() {
408@@ -202,7 +200,8 @@
409
410 it('can set a service config', function() {
411 var config = {'cfg-key': 'cfg-val'};
412- env.set_config('mysql', config);
413+ // This also tests that it only sends changed values.
414+ env.set_config('mysql', config, null, {});
415 msg = conn.last_message();
416 assert.equal(msg.op, 'set_config');
417 assert.equal(msg.service_name, 'mysql');
418@@ -213,7 +212,7 @@
419 /*jshint multistr:true */
420 var data = 'tuning-level: \nexpert-mojo';
421 /*jshint multistr:false */
422- env.set_config('mysql', null, data);
423+ env.set_config('mysql', null, data, null);
424 msg = conn.last_message();
425 assert.equal(msg.op, 'set_config');
426 assert.equal(msg.service_name, 'mysql');
427@@ -222,7 +221,7 @@
428
429 it('handles failed set config', function() {
430 var err, service_name;
431- env.set_config('yoursql', {}, null, function(evt) {
432+ env.set_config('yoursql', {}, null, {}, function(evt) {
433 err = evt.err;
434 service_name = evt.service_name;
435 });
436@@ -479,7 +478,7 @@
437 });
438
439 it('denies changes to config options if the GUI is read-only', function() {
440- assertOperationDenied('set_config', ['haproxy', {}, null]);
441+ assertOperationDenied('set_config', ['haproxy', {}, null, {}]);
442 });
443
444 it('denies changing constraints if the GUI is read-only', function() {
445
446=== modified file 'test/test_fakebackend.js'
447--- test/test_fakebackend.js 2013-09-02 16:37:49 +0000
448+++ test/test_fakebackend.js 2013-09-03 15:47:40 +0000
449@@ -121,7 +121,12 @@
450 annotations: {},
451 aggregated_status: undefined,
452 charm: 'cs:precise/wordpress-15',
453- config: undefined,
454+ config: {
455+ debug: 'no',
456+ engine: 'nginx',
457+ tuning: 'single',
458+ 'wp-content': ''
459+ },
460 constraints: {},
461 constraintsStr: undefined,
462 destroyed: false,
463@@ -216,8 +221,13 @@
464
465 it('accepts a config.', function() {
466 fakebackend.deploy(
467- 'cs:precise/wordpress-15', callback, {config: {funny: 'business'}});
468- assert.deepEqual(result.service.get('config'), {funny: 'business'});
469+ 'cs:precise/wordpress-15', callback, {config: {engine: 'apache'}});
470+ assert.deepEqual(result.service.get('config'), {
471+ debug: 'no',
472+ engine: 'apache',
473+ tuning: 'single',
474+ 'wp-content': ''
475+ });
476 });
477
478 it('deploys multiple units.', function() {
479@@ -247,8 +257,13 @@
480 fakebackend.deploy(
481 'cs:precise/wordpress-15',
482 callback,
483- {config: {funny: 'business'}, configYAML: 'funny: girl'});
484- assert.deepEqual(result.service.get('config'), {funny: 'girl'});
485+ {config: {funny: 'business'}, configYAML: 'engine: apache'});
486+ assert.deepEqual(result.service.get('config'), {
487+ debug: 'no',
488+ engine: 'apache',
489+ tuning: 'single',
490+ 'wp-content': ''
491+ });
492 });
493
494 it('rejects a non-string configYAML', function() {
495@@ -264,7 +279,7 @@
496 {configYAML:
497 Y.io('assets/mysql-config.yaml', {sync: true}).responseText});
498 assert.isObject(result.service.get('config'));
499- assert.equal(result.service.get('config')['tuning-level'], 'super bad');
500+ assert.equal(result.service.get('config').tuning, 'super bad');
501 });
502
503 it('rejects unparseable YAML config string.', function() {
504
505=== modified file 'test/test_ghost_inspector.js'
506--- test/test_ghost_inspector.js 2013-08-27 16:30:39 +0000
507+++ test/test_ghost_inspector.js 2013-09-03 15:47:40 +0000
508@@ -139,11 +139,7 @@
509 var message = env.ws.last_message();
510 var params = message.Params;
511 var config = {
512- admins: '',
513- debug: 'false',
514- logo: '',
515- name: 'foo',
516- skin: 'vector'
517+ name: 'foo'
518 };
519 assert.equal('ServiceDeploy', message.Request);
520 assert.equal('mediawiki', params.ServiceName);
521@@ -189,7 +185,7 @@
522 vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click');
523
524 var message = env.ws.last_message();
525- assert.equal(message.constraints.cpu, '2');
526+ assert.deepEqual(message.constraints, ['cpu=2', 'mem=', 'arch=']);
527 });
528
529 it('deploys with constraints in go env', function() {
530@@ -204,7 +200,7 @@
531 vmContainer.one('.viewlet-manager-footer button.confirm').simulate('click');
532
533 var message = env.ws.last_message();
534- assert.equal(message.Params.Constraints['cpu-power'], '2');
535+ assert.deepEqual(message.Params.Constraints, { 'cpu-power': 2 });
536 });
537
538 it('disables and resets input fields when \'use default config\' is active',
539
540=== modified file 'test/test_sandbox_go.js'
541--- test/test_sandbox_go.js 2013-09-02 16:42:27 +0000
542+++ test/test_sandbox_go.js 2013-09-03 15:47:40 +0000
543@@ -212,6 +212,12 @@
544 'Exposed': false,
545 'CharmURL': 'cs:precise/wordpress-15',
546 'Life': 'alive',
547+ 'Config': {
548+ debug: 'no',
549+ engine: 'nginx',
550+ tuning: 'single',
551+ 'wp-content': ''
552+ },
553 'Constraints': {}
554 }],
555 ['machine', 'change', {'Status': 'running'}],
556@@ -238,7 +244,7 @@
557 Params: {
558 CharmUrl: 'cs:precise/wordpress-15',
559 ServiceName: 'kumquat',
560- ConfigYAML: 'funny: business',
561+ ConfigYAML: 'engine: apache',
562 NumUnits: 2
563 },
564 RequestId: 42
565@@ -252,7 +258,12 @@
566 var service = state.db.services.getById('kumquat');
567 assert.isObject(service);
568 assert.equal(service.get('charm'), 'cs:precise/wordpress-15');
569- assert.deepEqual(service.get('config'), {funny: 'business'});
570+ assert.deepEqual(service.get('config'), {
571+ debug: 'no',
572+ engine: 'apache',
573+ tuning: 'single',
574+ 'wp-content': ''
575+ });
576 var units = state.db.units.get_units_for_service(service);
577 assert.lengthOf(units, 2);
578 done();
579@@ -269,12 +280,17 @@
580 assert.equal(result.charm_url, 'cs:precise/wordpress-15');
581 var service = state.db.services.getById('kumquat');
582 assert.equal(service.get('charm'), 'cs:precise/wordpress-15');
583- assert.deepEqual(service.get('config'), {llama: 'pajama'});
584+ assert.deepEqual(service.get('config'), {
585+ debug: 'no',
586+ engine: 'apache',
587+ tuning: 'single',
588+ 'wp-content': ''
589+ });
590 };
591 env.deploy(
592 'cs:precise/wordpress-15',
593 'kumquat',
594- {llama: 'pajama'},
595+ {engine: 'apache'},
596 null,
597 1,
598 null,
599@@ -440,7 +456,7 @@
600 Request: 'ServiceSet',
601 Params: {
602 ServiceName: 'wordpress',
603- Config: { llama: 'pajama' }
604+ Config: { engine: 'apache' }
605 },
606 RequestId: 42
607 };
608@@ -448,7 +464,12 @@
609 var receivedData = Y.JSON.parse(received.data);
610 assert.isUndefined(receivedData.Error);
611 var service = state.db.services.getById('wordpress');
612- assert.deepEqual(service.get('config'), { llama: 'pajama' });
613+ assert.deepEqual(service.get('config'), {
614+ debug: 'no',
615+ engine: 'apache',
616+ tuning: 'single',
617+ 'wp-content': ''
618+ });
619 done();
620 };
621 client.open();
622@@ -462,10 +483,17 @@
623 var callback = function(result) {
624 assert.isUndefined(result.err);
625 var service = state.db.services.getById('wordpress');
626- assert.deepEqual(service.get('config'), { llama: 'pajama' });
627+ assert.deepEqual(service.get('config'), {
628+ debug: 'no',
629+ engine: 'apache',
630+ tuning: 'single',
631+ 'wp-content': ''
632+ });
633 done();
634 };
635- env.set_config('wordpress', {llama: 'pajama'}, undefined, callback);
636+ env.set_config('wordpress', {
637+ engine: 'apache'
638+ }, undefined, {}, callback);
639 });
640 });
641
642@@ -476,7 +504,7 @@
643 Request: 'ServiceSetYAML',
644 Params: {
645 ServiceName: 'wordpress',
646- ConfigYAML: 'wordpress:\n llama: pajama'
647+ ConfigYAML: 'wordpress:\n engine: apache'
648 },
649 RequestId: 42
650 };
651@@ -484,7 +512,12 @@
652 var receivedData = Y.JSON.parse(received.data);
653 assert.isUndefined(receivedData.Error);
654 var service = state.db.services.getById('wordpress');
655- assert.deepEqual(service.get('config'), { llama: 'pajama' });
656+ assert.deepEqual(service.get('config'), {
657+ debug: 'no',
658+ engine: 'apache',
659+ tuning: 'single',
660+ 'wp-content': ''
661+ });
662 done();
663 };
664 client.open();
665@@ -498,11 +531,16 @@
666 var callback = function(result) {
667 assert.isUndefined(result.err);
668 var service = state.db.services.getById('wordpress');
669- assert.deepEqual(service.get('config'), { llama: 'pajama' });
670+ assert.deepEqual(service.get('config'), {
671+ debug: 'no',
672+ engine: 'apache',
673+ tuning: 'single',
674+ 'wp-content': ''
675+ });
676 done();
677 };
678 env.set_config('wordpress', undefined,
679- 'wordpress:\n llama: pajama', callback);
680+ 'wordpress:\n engine: apache', {}, callback);
681 });
682 });
683
684
685=== modified file 'test/test_sandbox_python.js'
686--- test/test_sandbox_python.js 2013-08-22 19:04:38 +0000
687+++ test/test_sandbox_python.js 2013-09-03 15:47:40 +0000
688@@ -341,7 +341,7 @@
689 op: 'deploy',
690 charm_url: 'cs:precise/wordpress-15',
691 service_name: 'kumquat',
692- config_raw: 'funny: business',
693+ config_raw: 'engine: apache',
694 num_units: 2,
695 request_id: 42
696 };
697@@ -358,7 +358,12 @@
698 var service = state.db.services.getById('kumquat');
699 assert.isObject(service);
700 assert.equal(service.get('charm'), 'cs:precise/wordpress-15');
701- assert.deepEqual(service.get('config'), {funny: 'business'});
702+ assert.deepEqual(service.get('config'), {
703+ debug: 'no',
704+ engine: 'apache',
705+ tuning: 'single',
706+ 'wp-content': ''
707+ });
708 var units = state.db.units.get_units_for_service(service);
709 assert.lengthOf(units, 2);
710 done();
711@@ -376,13 +381,18 @@
712 assert.equal(result.charm_url, 'cs:precise/wordpress-15');
713 var service = state.db.services.getById('kumquat');
714 assert.equal(service.get('charm'), 'cs:precise/wordpress-15');
715- assert.deepEqual(service.get('config'), {llama: 'pajama'});
716+ assert.deepEqual(service.get('config'), {
717+ debug: 'no',
718+ engine: 'apache',
719+ tuning: 'single',
720+ 'wp-content': ''
721+ });
722 done();
723 };
724 env.deploy(
725 'cs:precise/wordpress-15',
726 'kumquat',
727- {llama: 'pajama'},
728+ {engine: 'apache'},
729 null,
730 1,
731 null,
732@@ -799,14 +809,19 @@
733 var op = {
734 op: 'set_config',
735 service_name: 'wordpress',
736- config: {'blog-title': 'Inimical'},
737+ config: {'engine': 'apache'},
738 request_id: 99
739 };
740 client.onmessage = function(received) {
741 var parsed = Y.JSON.parse(received.data);
742- assert.deepEqual(parsed.result, {'blog-title': 'Inimical'});
743+ assert.deepEqual(parsed.result, {
744+ debug: 'no',
745+ engine: 'apache',
746+ tuning: 'single',
747+ 'wp-content': ''
748+ });
749 var service = state.db.services.getById('wordpress');
750- assert.equal(service.get('config')['blog-title'], 'Inimical');
751+ assert.equal(service.get('config').engine, 'apache');
752 // Error should be undefined.
753 done(parsed.error);
754 };
755
756=== modified file 'test/test_service_config_view.js'
757--- test/test_service_config_view.js 2013-08-27 16:24:00 +0000
758+++ test/test_service_config_view.js 2013-09-03 15:47:40 +0000
759@@ -160,7 +160,8 @@
760 assert.equal('ServiceSet', message.Request);
761 assert.equal('mysql', params.ServiceName);
762 assert.equal('new value', params.Config.option0);
763- assert.equal('true', params.Config.option1);
764+ // undefined because it should only set the changed values
765+ assert.equal(undefined, params.Config.option1);
766 });
767
768 it('should reenable the "Update" button if RPC fails', function() {
769@@ -168,7 +169,7 @@
770 var save_button = container.one('#save-service-config');
771 save_button.get('disabled').should.equal(shouldBe);
772 };
773- env.set_config = function(service, config, data, callback) {
774+ env.set_config = function(service, config, data, serviceCfg, callback) {
775 assertButtonDisabled(true);
776 callback(ev);
777 };
778@@ -226,7 +227,7 @@
779 // Mock function
780 // view.saveConfig() calls it as part of its internal
781 // "success" callback
782- env.set_config = function(service, config, data, callback) {
783+ env.set_config = function(service, config, data, serviceCfg, callback) {
784 callback(ev);
785 };
786 var ev = {err: false},
787
788=== modified file 'test/test_utils.js'
789--- test/test_utils.js 2013-08-19 20:51:17 +0000
790+++ test/test_utils.js 2013-09-03 15:47:40 +0000
791@@ -140,7 +140,7 @@
792 before(function() {
793 getConstraints = utils.getConstraints;
794 serviceConstraints = {arch: 'lcars', cpu: 'quantum', mem: 'teraflop'};
795- customConstraints = {foo: 'bar', arch: 'amd64', mem: '1GB'};
796+ customConstraints = {foo: 'bar', arch: 'amd64', mem: '1MB'};
797 genericConstraints = ['cpu', 'mem', 'arch'];
798 readOnlyConstraints = utils.readOnlyConstraints;
799 constraintDescriptions = utils.constraintDescriptions;
800@@ -149,7 +149,7 @@
801 it('correctly returns constraints for a service', function() {
802 var expected = [
803 {name: 'cpu', value: 'quantum', title: 'CPU', unit: 'GHz'},
804- {name: 'mem', value: 'teraflop', title: 'Memory', unit: 'GB'},
805+ {name: 'mem', value: 'teraflop', title: 'Memory', unit: 'MB'},
806 {name: 'arch', value: 'lcars', title: 'Architecture'}
807 ];
808 var obtained = getConstraints(serviceConstraints, genericConstraints);
809@@ -161,7 +161,7 @@
810 it('handles missing service constraints', function() {
811 var expected = [
812 {name: 'cpu', value: '', title: 'CPU', unit: 'GHz'},
813- {name: 'mem', value: '', title: 'Memory', unit: 'GB'},
814+ {name: 'mem', value: '', title: 'Memory', unit: 'MB'},
815 {name: 'arch', value: '', title: 'Architecture'}
816 ];
817 var obtained = getConstraints({}, genericConstraints);
818@@ -171,7 +171,7 @@
819 it('includes unexpected service constraints', function() {
820 var expected = [
821 {name: 'cpu', value: '', title: 'CPU', unit: 'GHz'},
822- {name: 'mem', value: '1GB', title: 'Memory', unit: 'GB'},
823+ {name: 'mem', value: '1MB', title: 'Memory', unit: 'MB'},
824 {name: 'arch', value: 'amd64', title: 'Architecture'},
825 {name: 'foo', value: 'bar', title: 'foo'}
826 ];

Subscribers

People subscribed via source and target branches