Merge lp:~dstroppa/charms/precise/node-app/refactor-hooks into lp:charms/node-app

Proposed by Daniele Stroppa
Status: Work in progress
Proposed branch: lp:~dstroppa/charms/precise/node-app/refactor-hooks
Merge into: lp:charms/node-app
Diff against target: 1377 lines (+679/-367)
20 files modified
.bzrignore (+3/-0)
config.yaml (+2/-2)
config/config.js (+10/-0)
hooks/config-changed (+0/-137)
hooks/install (+0/-64)
hooks/install-app (+14/-0)
hooks/mongodb-relation-changed (+0/-50)
hooks/mongodb-relation-departed (+0/-4)
hooks/start (+9/-3)
hooks/stop (+0/-5)
hooks/website-relation-changed (+0/-4)
icon.svg (+37/-97)
lib/app.js (+435/-0)
lib/charm.js (+75/-0)
metadata.yaml (+1/-1)
tests/00-setup (+5/-0)
tests/01-mongodb-deployment (+31/-0)
tests/02-mongodb-haproxy-deployment (+33/-0)
tpl/mongodb_env.tpl (+8/-0)
tpl/upstart.tpl (+16/-0)
To merge this branch: bzr merge lp:~dstroppa/charms/precise/node-app/refactor-hooks
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+200330@code.launchpad.net

Description of the change

Hooks have been re-written in JS

To post a comment you must log in.
Revision history for this message
Marco Ceppi (marcoceppi) wrote :

# Review

- I don't understand what expose_app is for, how does this differ from juju expose node-app?

- Excellent use of app_user, however it's not being used in the upstart script. Consider using the following as a reference: https://gist.github.com/marcoceppi/8458344

- I really like the way lib/charm.js is shaping up, I'm confident this will be a key part in helping other node.js authors write hooks/charms in node.js Along the same vein I think methods like installPackage, installApp, updateApp, configureApp, etc are fantastic, but don't seem inheriently a part of Charm itself. As an opinion it might be better served breaking the two apart. Charm to emulate all the core juju functions in node then a seperat App module with these methods.

- The charm needs more logging to juju-log, most of the comments you have in the final callbacks are great, sending them to juju-log will notify the user what's going on and help debug issues

- juju unexpose (and expose) aren't executed from the charm but instead by the user, as a result the mongodb-relations-departed and stop hooks need to be updated. Instead the charm needs to tell juju /what/ports to open by calling `open-port` and `close-port` These won't explicitly open or close the ports, but tells juju what ports to open in the firewall when the user run juju expose/unexpose.

# Knitpick

- I really like the idea of a polling_schedule, as an alternative it might be nice to have a push_url, where the user could specify a token as a configuration option, then have a url like http://ip:2222/update?key=<key-they-set> so they could have github push notifications on updates and other repositories instead of a constant polling. Just an idea.

- Having config/config.js where it is, I thought it was something the user filled out. I fear other users may also make this assumption. I'm not sure of a better place just making you aware of the perception.

- You'll want to update the maintainer field in the metadata.yaml file

- Again, not sure if this is a common practice in node, but relation_set seems to take a list of keys and values, would it be better served as an object of key: val pairs?

Sorry this review was so...breif...in words. I honestly love the direction of the charm so far and understand it's still under development. These were things that popped out at me doing a comb through the charm. Let me know if you have any question, happy to discuss these points further.

I'm going to move the merge request to Work In Progress, feel free to email me when you'd like another look or just move the merge to "Needs Review"

58. By Daniele Stroppa

Changes after review: moved app specific method into lib/app.js, updated upstart script, new icon

Revision history for this message
Daniele Stroppa (dstroppa) wrote :
Download full text (3.3 KiB)

Comments inline.

>
> - I don't understand what expose_app is for, how does this differ from
> juju expose node-app?
>

It's actually not different, and seeing your other comment below (" juju
unexpose (and expose) aren't executed from the charm but instead by the
user") I've removed this.

> - Excellent use of app_user, however it's not being used in the upstart
> script. Consider using the following as a reference:
> https://gist.github.com/marcoceppi/8458344

Modified the upstart script according to this reference.

- I really like the way lib/charm.js is shaping up, I'm confident this will
> be a key part in helping other node.js authors write hooks/charms in
> node.js Along the same vein I think methods like installPackage,
> installApp, updateApp, configureApp, etc are fantastic, but don't seem
> inheriently a part of Charm itself. As an opinion it might be better served
> breaking the two apart. Charm to emulate all the core juju functions in
> node then a seperat App module with these methods.
>

This is actually a good point. I've moved out of lib/charm.js all methods
that are app related (now in lib/app.js)

>
> - The charm needs more logging to juju-log, most of the comments you have
> in the final callbacks are great, sending them to juju-log will notify the
> user what's going on and help debug issues
>

Agreed, added logging to juju-log.

>
> - juju unexpose (and expose) aren't executed from the charm but instead by
> the user, as a result the mongodb-relations-departed and stop hooks need to
> be updated. Instead the charm needs to tell juju /what/ports to open by
> calling `open-port` and `close-port` These won't explicitly open or close
> the ports, but tells juju what ports to open in the firewall when the user
> run juju expose/unexpose.
>

Done.

>
> # Knitpick
>
> - I really like the idea of a polling_schedule, as an alternative it might
> be nice to have a push_url, where the user could specify a token as a
> configuration option, then have a url like http://ip:2222/update?key=<key-they-set>
> so they could have github push notifications on updates and other
> repositories instead of a constant polling. Just an idea.
>
> - Having config/config.js where it is, I thought it was something the user
> filled out. I fear other users may also make this assumption. I'm not sure
> of a better place just making you aware of the perception.
>
> - You'll want to update the maintainer field in the metadata.yaml file
>

Done :)

>
> - Again, not sure if this is a common practice in node, but relation_set
> seems to take a list of keys and values, would it be better served as an
> object of key: val pairs?
>

Agreed, that's a better way.

>
> Sorry this review was so...breif...in words. I honestly love the direction
> of the charm so far and understand it's still under development. These were
> things that popped out at me doing a comb through the charm. Let me know if
> you have any question, happy to discuss these points further.
>
> I'm going to move the merge request to Work In Progress, feel free to
> email me when you'd like another look or just move the merge to "Needs
> Review"
> --
>
> https://code.launchpad.net/~dstro...

Read more...

59. By Daniele Stroppa

Adding Amulet tests

Unmerged revisions

59. By Daniele Stroppa

Adding Amulet tests

58. By Daniele Stroppa

Changes after review: moved app specific method into lib/app.js, updated upstart script, new icon

57. By Daniele Stroppa

Fixing relation-set and website-relation-changed hook

56. By Daniele Stroppa

Added flag to expose the app or use proxy

55. By Daniele Stroppa

code cleanup, added close-port on stop

54. By Daniele Stroppa

mongodb-relation-changed, config-changed hooks tested

53. By Daniele Stroppa

Testing/Debuggin hooks

52. By Daniele Stroppa

Hooks debugging - install hook fixed

51. By Daniele Stroppa

Hooks debugging

50. By Daniele Stroppa

Hooks Debugging/Testing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2014-01-24 18:05:39 +0000
4@@ -0,0 +1,3 @@
5+.idea
6+node-app.iml
7+hooks.old
8
9=== added directory 'config'
10=== modified file 'config.yaml'
11--- config.yaml 2013-03-08 13:06:46 +0000
12+++ config.yaml 2014-01-24 18:05:39 +0000
13@@ -10,10 +10,10 @@
14 app_scm:
15 type: string
16 default: git
17- description: Repository type (git/bzr/...)
18+ description: Repository type (git/bzr/subversion/mercurial)
19 app_url:
20 type: string
21- default: "http://github.com/mmm/testnode.git"
22+ default: "https://github.com/dstroppa/testnode.git"
23 description: Application repository URL
24 app_branch:
25 type: string
26
27=== added file 'config/config.js'
28--- config/config.js 1970-01-01 00:00:00 +0000
29+++ config/config.js 2014-01-24 18:05:39 +0000
30@@ -0,0 +1,10 @@
31+{
32+ 'name': '',
33+ 'listen_port': 0,
34+ 'database': {
35+ 'mongo_host': '',
36+ 'mongo_port': '',
37+ 'mongo_replset': ''
38+ },
39+
40+}
41\ No newline at end of file
42
43=== added file 'hooks/config-changed'
44--- hooks/config-changed 1970-01-01 00:00:00 +0000
45+++ hooks/config-changed 2014-01-24 18:05:39 +0000
46@@ -0,0 +1,14 @@
47+#!/usr/bin/env node
48+
49+var app = require('../lib/app')();
50+var charm = require('../lib/charm')();
51+var util = require('util');
52+
53+charm.config_get_all(function(config) {
54+ app.updateApp(config, function() {
55+ app.configureApp(config, function() {
56+ // app updated and configured successfully
57+ charm.log(util.format('%s updated and configured successfully.', config.app_name));
58+ });
59+ });
60+});
61\ No newline at end of file
62
63=== removed file 'hooks/config-changed'
64--- hooks/config-changed 2013-04-04 12:42:58 +0000
65+++ hooks/config-changed 1970-01-01 00:00:00 +0000
66@@ -1,137 +0,0 @@
67-#!/bin/bash
68-
69-set -eu # -x for verbose logging to juju debug-log
70-
71-umask 002
72-
73-install_root=`config-get install_root`
74-app_name=`config-get app_name`
75-app_dir="$install_root/$app_name"
76-app_user=`config-get app_user`
77-app_branch=`config-get app_branch`
78-app_port=`config-get app_port`
79-app_node_env=`config-get app_node_env`
80-cron_schedule=`config-get polling_schedule`
81-
82-update_app() {
83- cd $app_dir
84-
85- is_branch=false
86-
87- # Always fetch latest if branch requested
88- if git checkout origin/${app_branch}; then # is a local branch
89- git fetch origin && git checkout origin/${app_branch}
90- is_branch=true
91- else # tag/commit ref?
92- if ! git checkout ${app_branch}; then # commit does not exist locally
93- git fetch origin
94- if ! git checkout origin/${app_branch}; then # not a new remote branch, is a commit ref/tag
95- git checkout $app_branch
96- else
97- is_branch=true
98- fi
99- fi
100- fi
101-}
102-
103-configure_app() {
104- juju-log "Configuring ${app_name}..."
105-
106- # If Procfile found, use it with foreman module to create upstart script
107- if [ -f ${app_dir}/Procfile ]; then
108- juju-log "Using Procfile to configure ${app_name} using foreman..."
109-
110- if [ -f /etc/juju_nodejs_app_${app_name}_mongodb.env ]; then
111- mongodb_config=`cat /etc/juju_nodejs_app_${app_name}_mongodb.env`
112- else
113- mongodb_config='""'
114- fi
115-
116- cat > /etc/juju_nodejs_app_${app_name}.env <<EOS
117-{
118- "name": "${app_name}"
119- , "node_env": "${app_node_env}"
120- , "mongo": ${mongodb_config}
121-}
122-EOS
123-
124- cd $app_dir # foreman export uses this as root
125- nf export -j ${app_dir}/Procfile -e /etc/juju_nodejs_app_${app_name}.env -a ${app_name} -u ${app_user} -p ${app_port} -o /etc/init -t upstart
126- else
127- config_file_path=${app_dir}/config/config.js
128- if [ -f $config_file_path ]; then
129- juju-log "Writing $app_name config file $config_file_path"
130- sed -i "s/name.*/name\" : \"${app_name}\"/" $config_file_path
131- sed -i "s/listen_port.*/listen_port\" : \"${app_port}\"/" $config_file_path
132- fi
133- fi
134-}
135-
136-install_cronjob() {
137- update_cron_job_filename=/opt/bin/update_node-app_from_SCM.sh
138-
139- if [ $is_branch ]; then
140- if [ ! -d `dirname $update_cron_job_filename` ]; then
141- mkdir -p `dirname $update_cron_job_filename`
142- fi
143-
144- cat > $update_cron_job_filename <<EOS
145-
146-#!/usr/bin/env bash
147-
148-cd \${app_dir}
149-
150-old_commit=\`git rev-parse origin/\${app_branch}\`
151-
152-echo "Checking for updates to \${app_name} branch \${app_branch} commit \${old_commit}"
153-git fetch origin
154-
155-new_commit=\`git rev-parse origin/\${app_branch}\`
156-
157-if [ \$old_commit != \$new_commit ]; then
158- echo "Found new latest commit \${new_commit}"
159-
160- git checkout origin/\${app_branch}
161- npm update && npm install
162- sudo service \${app_name} restart
163-else
164- echo 'No updates found';
165-fi
166-EOS
167-
168- chmod u=rwx,g=r,o=r $update_cron_job_filename
169- chown ${app_user} $update_cron_job_filename
170-
171- cat > $update_cron_d_file <<EOF
172-app_branch=$app_branch
173-app_name=$app_name
174-app_dir=$app_dir
175-
176-$cron_schedule $app_user $update_cron_job_filename
177-EOF
178- chmod u=rwx,g=r,o=r $update_cron_d_file
179- else
180- juju-log "Ignoring polling for new code as commit/tag specified"
181-
182- if [ -f $update_cron_d_file ]; then
183- rm $update_cron_d_file
184- fi
185- fi
186-}
187-
188-update_app
189-configure_app
190-
191-update_cron_d_file=/etc/cron.d/${app_name} # TODO: what happens if app_name changes?
192-
193-if [ "$cron_schedule" != "" ]; then
194- install_cronjob
195-elif [ -f $update_cron_d_file ]; then
196- rm $update_cron_d_file
197-fi
198-
199-npm update && npm install
200-
201-chown -Rf ${app_user}.${app_user} ${app_dir}
202-
203-service ${app_name} restart
204
205=== added file 'hooks/install'
206--- hooks/install 1970-01-01 00:00:00 +0000
207+++ hooks/install 2014-01-24 18:05:39 +0000
208@@ -0,0 +1,20 @@
209+#!/bin/bash
210+
211+set -eu # -x for verbose logging to juju debug-log
212+
213+umask 002
214+
215+install_node() {
216+ juju-log "Installing node..."
217+ add-apt-repository ppa:chris-lea/node.js
218+ apt-get update
219+ apt-get -y install -qq nodejs build-essential curl
220+}
221+[[ -x /usr/bin/node ]] || install_node
222+
223+modules=( mustache uid-number vasync )
224+for module in ${modules[@]}; do
225+ npm install $module
226+done
227+
228+./hooks/install-app
229\ No newline at end of file
230
231=== removed file 'hooks/install'
232--- hooks/install 2013-03-26 21:18:19 +0000
233+++ hooks/install 1970-01-01 00:00:00 +0000
234@@ -1,64 +0,0 @@
235-#!/bin/bash
236-
237-set -eu # -x for verbose logging to juju debug-log
238-
239-umask 002
240-
241-install_root=`config-get install_root`
242-app_name=`config-get app_name`
243-app_dir="$install_root/$app_name"
244-app_user=`config-get app_user`
245-app_scm=`config-get app_scm`
246-app_url=`config-get app_url`
247-app_branch=`config-get app_branch`
248-app_port=`config-get app_port`
249-
250-apt-get -y install -qq git-core
251-
252-install_node() {
253- juju-log "Installing node..."
254- add-apt-repository ppa:chris-lea/node.js
255- apt-get update
256- apt-get -y install -qq nodejs build-essential curl
257-}
258-[[ -x /usr/bin/node ]] || install_node
259-
260-install_app() {
261- juju-log "Installing ${app_name}..."
262- git clone ${app_url} ${app_dir} -b ${app_branch}
263- chown -Rf ${app_user}.${app_user} ${app_dir}
264-
265- if [ -f ${app_dir}/package.json ]; then
266- cd ${app_dir} && npm install
267- fi
268-
269- # If Procfile found, use it with foreman module
270- if [ -f ${app_dir}/Procfile ]; then
271- npm install -g foreman
272- else
273- cat > /etc/init/${app_name}.conf <<EOS
274-description "${app_name} node.js server"
275-
276-start on (net-device-up
277- and local-filesystems
278- and runlevel [2345])
279-stop on runlevel [!2345]
280-
281-expect fork
282-respawn
283-
284-script
285- export HOME=/
286- export NODE_PATH=/usr/lib/node
287- #exec sudo -u ${app_user} /usr/bin/node ${app_dir}/server.js >> /var/log/${app_name}.log 2>&1 &
288- exec /usr/bin/node ${app_dir}/server.js >> /var/log/${app_name}.log 2>&1 &
289-end script
290-EOS
291- fi
292-}
293-[[ -d ${app_dir} ]] || install_app
294-
295-juju-log "Delaying app startup until mongodb joins"
296-#juju-log "starting app"
297-#service ${app_name} restart || service ${app_name} start
298-
299
300=== added file 'hooks/install-app'
301--- hooks/install-app 1970-01-01 00:00:00 +0000
302+++ hooks/install-app 2014-01-24 18:05:39 +0000
303@@ -0,0 +1,14 @@
304+#!/usr/bin/env node
305+
306+var app = require('../lib/app')();
307+var charm = require('../lib/charm')();
308+var util = require('util');
309+
310+charm.config_get_all(function(config) {
311+ app.installPackage(config.app_scm, function() {
312+ app.installApp(config, function() {
313+ // All done
314+ charm.log(util.format('%s installed successfully.', config.app_name));
315+ });
316+ });
317+});
318\ No newline at end of file
319
320=== added file 'hooks/mongodb-relation-changed'
321--- hooks/mongodb-relation-changed 1970-01-01 00:00:00 +0000
322+++ hooks/mongodb-relation-changed 2014-01-24 18:05:39 +0000
323@@ -0,0 +1,12 @@
324+#!/usr/bin/env node
325+
326+var app = require('../lib/app')();
327+var charm = require('../lib/charm')();
328+var util = require('util');
329+
330+charm.config_get_all(function(config) {
331+ app.reconfigureApp(config, function() {
332+ //all done
333+ charm.log(util.format('%s configured and started successfully', config.app_name));
334+ });
335+});
336
337=== removed file 'hooks/mongodb-relation-changed'
338--- hooks/mongodb-relation-changed 2013-02-21 11:40:29 +0000
339+++ hooks/mongodb-relation-changed 1970-01-01 00:00:00 +0000
340@@ -1,50 +0,0 @@
341-#!/bin/bash
342-
343-set -eu
344-
345-# Get the database settings; if not set, wait for this hook to be
346-# invoked again
347-host=`relation-get private-address`
348-if [ -z "$host" ] ; then
349- exit 0 # wait for future handshake from database service unit
350-fi
351-
352-relation_port=`relation-get port`
353-port=${relation_port:-27017}
354-
355-replset=`relation-get replset`
356-
357-install_root=`config-get install_root`
358-app_name=`config-get app_name`
359-app_dir="$install_root/$app_name"
360-
361-configure_app() {
362- juju-log "configuring ${app_name} to work with the mongodb service"
363-
364- config_file_path=$app_dir/config/config.js
365-
366- if [ -f ${app_dir}/Procfile ]; then
367- cat > /etc/juju_nodejs_app_${app_name}_mongodb.env <<EOF
368-{
369- "host": "${host}"
370-, "port": ${port}
371-, "replset": "${replset}"
372-}
373-EOF
374- MY_DIR=`dirname $0`
375- $MY_DIR/config-changed
376- elif [ -f $config_file_path ]; then
377- juju-log "Writing $app_name config file $config_file_path"
378- sed -i "s/mongo_host.*/mongo_host\" : \"${host}\"/" $config_file_path
379- sed -i "s/mongo_port.*/mongo_port\" : ${port}/" $config_file_path
380- sed -i "s/mongo_replset.*/mongo_replset\" : \"${replset}\"/" $config_file_path
381- fi
382-
383- app_port=`config-get app_port`
384- open-port $app_port/TCP
385-}
386-configure_app
387-
388-juju-log "(re)starting app"
389-service ${app_name} restart || service ${app_name} start
390-
391
392=== added file 'hooks/mongodb-relation-departed'
393--- hooks/mongodb-relation-departed 1970-01-01 00:00:00 +0000
394+++ hooks/mongodb-relation-departed 2014-01-24 18:05:39 +0000
395@@ -0,0 +1,14 @@
396+#!/usr/bin/env node
397+
398+var app = require('../lib/app')();
399+var charm = require('../lib/charm')();
400+var util = require('util');
401+
402+charm.config_get_all(function(config) {
403+ app.exec('service', [config.app_name, 'status'], function(ret) {
404+ app.exec('service', [config.app_name, 'stop'], function(ret) {
405+ charm.close_port([config.app_port + '/TCP']);
406+ charm.log(util.format('%s stopped successfully.', config.app_name));
407+ });
408+ });
409+});
410
411=== removed file 'hooks/mongodb-relation-departed'
412--- hooks/mongodb-relation-departed 2011-12-03 00:08:39 +0000
413+++ hooks/mongodb-relation-departed 1970-01-01 00:00:00 +0000
414@@ -1,4 +0,0 @@
415-#!/bin/sh
416-
417-app_name=`config-get app_name`
418-service ${app_name} status && service ${app_name} stop
419
420=== modified file 'hooks/start'
421--- hooks/start 2011-09-07 12:30:23 +0000
422+++ hooks/start 2014-01-24 18:05:39 +0000
423@@ -1,3 +1,9 @@
424-#!/bin/bash
425-
426-# delay startup until mongo joins
427+#!/usr/bin/env node
428+
429+var charm = require('../lib/charm')();
430+
431+charm.config_get_all(function(config) {
432+ // delay start until mongo joins
433+ // app.exec('service', [config.app_name, 'start'], function(ret) {
434+ //});
435+});
436\ No newline at end of file
437
438=== added file 'hooks/stop'
439--- hooks/stop 1970-01-01 00:00:00 +0000
440+++ hooks/stop 2014-01-24 18:05:39 +0000
441@@ -0,0 +1,14 @@
442+#!/usr/bin/env node
443+
444+var app = require('../lib/app')();
445+var charm = require('../lib/charm')();
446+var util = require('util');
447+
448+charm.config_get_all(function(config) {
449+ app.exec('service', [config.app_name, 'status'], function(ret) {
450+ app.exec('service', [config.app_name, 'stop'], function(ret) {
451+ charm.close_port([config.app_port + '/TCP']);
452+ charm.log(util.format('%s stopped successfully.', config.app_name));
453+ });
454+ });
455+});
456\ No newline at end of file
457
458=== removed file 'hooks/stop'
459--- hooks/stop 2011-12-03 00:08:39 +0000
460+++ hooks/stop 1970-01-01 00:00:00 +0000
461@@ -1,5 +0,0 @@
462-#!/bin/bash
463-
464-app_name=`config-get app_name`
465-service ${app_name} status && service ${app_name} stop
466-
467
468=== removed symlink 'hooks/upgrade-charm'
469=== target was u'install'
470=== added file 'hooks/website-relation-changed'
471--- hooks/website-relation-changed 1970-01-01 00:00:00 +0000
472+++ hooks/website-relation-changed 2014-01-24 18:05:39 +0000
473@@ -0,0 +1,14 @@
474+#!/usr/bin/env node
475+
476+var charm = require('../lib/charm')();
477+var util = require('util');
478+
479+charm.config_get_all(function(config) {
480+ charm.unit_get('private-address', function(hostname) {
481+ charm.relation_set({'port' : config.app_port, 'hostname' : hostname}, function() {
482+ // relation successfully changed
483+ charm.log(util.format('%s website relation successfully set up.', config.app_name));
484+ });
485+ });
486+});
487+
488
489=== removed file 'hooks/website-relation-changed'
490--- hooks/website-relation-changed 2012-02-09 21:37:03 +0000
491+++ hooks/website-relation-changed 1970-01-01 00:00:00 +0000
492@@ -1,4 +0,0 @@
493-#!/bin/sh
494-
495-relation-set port=`config-get app_port` hostname=`unit-get private-address`
496-
497
498=== modified file 'icon.svg'
499--- icon.svg 2013-09-18 14:44:20 +0000
500+++ icon.svg 2014-01-24 18:05:39 +0000
501@@ -7,17 +7,27 @@
502 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
503 xmlns:svg="http://www.w3.org/2000/svg"
504 xmlns="http://www.w3.org/2000/svg"
505- xmlns:xlink="http://www.w3.org/1999/xlink"
506 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
507 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
508 width="96"
509 height="96"
510 id="svg6517"
511 version="1.1"
512- inkscape:version="0.48+devel r12505"
513- sodipodi:docname="nodejs.svg">
514+ inkscape:version="0.48.2 r9819"
515+ sodipodi:docname="icon.svg">
516 <defs
517 id="defs6519">
518+ <linearGradient
519+ id="Background">
520+ <stop
521+ id="stop4178"
522+ offset="0"
523+ style="stop-color:#b8b8b8;stop-opacity:1" />
524+ <stop
525+ id="stop4180"
526+ offset="1"
527+ style="stop-color:#c9c9c9;stop-opacity:1" />
528+ </linearGradient>
529 <filter
530 style="color-interpolation-filters:sRGB;"
531 inkscape:label="Inner Shadow"
532@@ -82,29 +92,6 @@
533 result="composite2"
534 id="feComposite960" />
535 </filter>
536- <linearGradient
537- inkscape:collect="always"
538- id="linearGradient902">
539- <stop
540- style="stop-color:#74b524;stop-opacity:1"
541- offset="0"
542- id="stop904" />
543- <stop
544- style="stop-color:#83cd29;stop-opacity:1"
545- offset="1"
546- id="stop906" />
547- </linearGradient>
548- <linearGradient
549- id="Background">
550- <stop
551- id="stop4178"
552- offset="0"
553- style="stop-color:#22779e;stop-opacity:1" />
554- <stop
555- id="stop4180"
556- offset="1"
557- style="stop-color:#2991c0;stop-opacity:1" />
558- </linearGradient>
559 <clipPath
560 clipPathUnits="userSpaceOnUse"
561 id="clipPath873">
562@@ -130,38 +117,6 @@
563 stdDeviation="0.71999962"
564 id="feGaussianBlur893" />
565 </filter>
566- <style
567- id="style867"
568- type="text/css"><![CDATA[
569- .fil0 {fill:#1F1A17}
570- ]]></style>
571- <linearGradient
572- inkscape:collect="always"
573- xlink:href="#linearGradient902"
574- id="linearGradient908"
575- x1="-220"
576- y1="731.29077"
577- x2="-220"
578- y2="635.29077"
579- gradientUnits="userSpaceOnUse" />
580- <clipPath
581- id="clipPath16">
582- <path
583- id="path18"
584- d="m -9,-9 614,0 0,231 -614,0 0,-231 z" />
585- </clipPath>
586- <clipPath
587- id="clipPath116">
588- <path
589- id="path118"
590- d="m 91.7368,146.3253 -9.7039,-1.577 -8.8548,-3.8814 -7.5206,-4.7308 -7.1566,-8.7335 -4.0431,-4.282 -3.9093,-1.4409 -1.034,2.5271 1.8079,2.6096 0.4062,3.6802 1.211,-0.0488 1.3232,-1.2069 -0.3569,3.7488 -1.4667,0.9839 0.0445,1.4286 -3.4744,-1.9655 -3.1462,-3.712 -0.6559,-3.3176 1.3453,-2.6567 1.2549,-4.5133 2.5521,-1.2084 2.6847,0.1318 2.5455,1.4791 -1.698,-8.6122 1.698,-9.5825 -1.8692,-4.4246 -6.1223,-6.5965 1.0885,-3.941 2.9002,-4.5669 5.4688,-3.8486 2.9007,-0.3969 3.225,-0.1094 -2.012,-8.2601 7.3993,-3.0326 9.2188,-1.2129 3.1535,2.0619 0.2427,5.5797 3.5178,5.8224 0.2426,4.6094 8.4909,-0.6066 7.8843,0.7279 -7.8843,-4.7307 1.3343,-5.701 4.9731,-7.763 4.8521,-2.0622 3.8814,1.5769 1.577,3.1538 8.1269,6.1861 1.5769,-1.3343 12.7363,-0.485 2.5473,2.0619 0.2426,3.6391 -0.849,1.5767 -0.6066,9.8251 -4.2454,8.4909 0.7276,3.7605 2.5475,-1.3343 7.1566,-6.6716 3.5175,-0.2424 3.8815,1.5769 3.8818,2.9109 1.9406,6.3077 11.4021,-0.7277 6.914,2.6686 5.5797,5.2157 4.0028,7.5206 0.9706,8.8546 -0.8493,10.3105 -2.1832,9.2185 -2.1836,2.9112 -3.0322,0.9706 -5.3373,-5.8224 -4.8518,-1.6982 -4.2455,7.0353 -4.2454,3.8815 -2.3049,1.4556 -9.2185,7.6419 -7.3993,4.0028 -7.3993,0.6066 -8.6119,-1.4556 -7.5206,-2.7899 -5.2158,-4.2454 -4.1241,-4.9734 -4.2454,-1.2129" />
591- </clipPath>
592- <clipPath
593- id="clipPath128">
594- <path
595- id="path130"
596- d="m 91.7368,146.3253 -9.7039,-1.577 -8.8548,-3.8814 -7.5206,-4.7308 -7.1566,-8.7335 -4.0431,-4.282 -3.9093,-1.4409 -1.034,2.5271 1.8079,2.6096 0.4062,3.6802 1.211,-0.0488 1.3232,-1.2069 -0.3569,3.7488 -1.4667,0.9839 0.0445,1.4286 -3.4744,-1.9655 -3.1462,-3.712 -0.6559,-3.3176 1.3453,-2.6567 1.2549,-4.5133 2.5521,-1.2084 2.6847,0.1318 2.5455,1.4791 -1.698,-8.6122 1.698,-9.5825 -1.8692,-4.4246 -6.1223,-6.5965 1.0885,-3.941 2.9002,-4.5669 5.4688,-3.8486 2.9007,-0.3969 3.225,-0.1094 -2.012,-8.2601 7.3993,-3.0326 9.2188,-1.2129 3.1535,2.0619 0.2427,5.5797 3.5178,5.8224 0.2426,4.6094 8.4909,-0.6066 7.8843,0.7279 -7.8843,-4.7307 1.3343,-5.701 4.9731,-7.763 4.8521,-2.0622 3.8814,1.5769 1.577,3.1538 8.1269,6.1861 1.5769,-1.3343 12.7363,-0.485 2.5473,2.0619 0.2426,3.6391 -0.849,1.5767 -0.6066,9.8251 -4.2454,8.4909 0.7276,3.7605 2.5475,-1.3343 7.1566,-6.6716 3.5175,-0.2424 3.8815,1.5769 3.8818,2.9109 1.9406,6.3077 11.4021,-0.7277 6.914,2.6686 5.5797,5.2157 4.0028,7.5206 0.9706,8.8546 -0.8493,10.3105 -2.1832,9.2185 -2.1836,2.9112 -3.0322,0.9706 -5.3373,-5.8224 -4.8518,-1.6982 -4.2455,7.0353 -4.2454,3.8815 -2.3049,1.4556 -9.2185,7.6419 -7.3993,4.0028 -7.3993,0.6066 -8.6119,-1.4556 -7.5206,-2.7899 -5.2158,-4.2454 -4.1241,-4.9734 -4.2454,-1.2129" />
597- </clipPath>
598 </defs>
599 <sodipodi:namedview
600 id="base"
601@@ -170,38 +125,25 @@
602 borderopacity="1.0"
603 inkscape:pageopacity="0.0"
604 inkscape:pageshadow="2"
605- inkscape:zoom="3.2596289"
606- inkscape:cx="62.012793"
607- inkscape:cy="45.550063"
608+ inkscape:zoom="4.0745362"
609+ inkscape:cx="43.548195"
610+ inkscape:cy="43.426863"
611 inkscape:document-units="px"
612- inkscape:current-layer="layer1"
613+ inkscape:current-layer="layer3"
614 showgrid="true"
615 fit-margin-top="0"
616 fit-margin-left="0"
617 fit-margin-right="0"
618 fit-margin-bottom="0"
619- inkscape:window-width="1920"
620- inkscape:window-height="1029"
621+ inkscape:window-width="1280"
622+ inkscape:window-height="687"
623 inkscape:window-x="0"
624- inkscape:window-y="24"
625+ inkscape:window-y="0"
626 inkscape:window-maximized="1"
627 showborder="true"
628 showguides="true"
629 inkscape:guide-bbox="true"
630- inkscape:showpageshadow="false"
631- inkscape:snap-global="true"
632- inkscape:snap-bbox="true"
633- inkscape:bbox-paths="true"
634- inkscape:bbox-nodes="true"
635- inkscape:snap-bbox-edge-midpoints="true"
636- inkscape:snap-bbox-midpoints="true"
637- inkscape:object-paths="true"
638- inkscape:snap-intersection-paths="true"
639- inkscape:object-nodes="true"
640- inkscape:snap-smooth-nodes="true"
641- inkscape:snap-midpoints="true"
642- inkscape:snap-object-midpoints="true"
643- inkscape:snap-center="true">
644+ inkscape:showpageshadow="false">
645 <inkscape:grid
646 type="xygrid"
647 id="grid821" />
648@@ -230,7 +172,7 @@
649 <dc:format>image/svg+xml</dc:format>
650 <dc:type
651 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
652- <dc:title></dc:title>
653+ <dc:title />
654 </cc:Work>
655 </rdf:RDF>
656 </metadata>
657@@ -239,29 +181,27 @@
658 inkscape:groupmode="layer"
659 id="layer1"
660 transform="translate(268,-635.29076)"
661- style="display:inline">
662+ style="display:inline"
663+ sodipodi:insensitive="true">
664 <path
665- style="fill:url(#linearGradient908);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"
666- d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"
667+ style="fill:#82cd29;fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"
668+ d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 c -27.21517,0 -31.10302,-3.89189 -31.10302,-31.13514 z"
669 id="path6455"
670 inkscape:connector-curvature="0"
671 sodipodi:nodetypes="sssssssss" />
672- <path
673- style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
674- d="m -220,651.29076 c -0.86298,0 -1.70107,0.17841 -2.44333,0.59855 l -23.49563,13.56741 c -1.52044,0.87539 -2.44457,2.53071 -2.44457,4.29059 l 0,27.0869 c 0,1.75892 0.92413,3.41126 2.44457,4.28936 l 6.13539,3.54211 c 2.98102,1.46918 4.08046,1.44658 5.43728,1.44658 4.41367,0 6.93426,-2.67777 6.93426,-7.33247 l 0,-26.73786 c 0,-0.37799 -0.32696,-0.64892 -0.69809,-0.64892 l -2.94356,0 c -0.37712,0 -0.6981,0.27093 -0.6981,0.64892 l 0,26.73786 c 0,2.06316 -2.14855,4.13777 -5.63639,2.39418 l -6.38611,-3.74122 c -0.22547,-0.12291 -0.39821,-0.33979 -0.39821,-0.59854 l 0,-27.0869 c 0,-0.25716 0.17073,-0.51883 0.39821,-0.64893 l 23.44524,-13.51826 c 0.21948,-0.12609 0.48183,-0.12609 0.69932,0 l 23.44401,13.51826 c 0.22348,0.13408 0.39945,0.38395 0.39945,0.64893 l 0,27.0869 c 0,0.25875 -0.12965,0.52304 -0.34906,0.6477 l -23.4944,13.51825 c -0.20153,0.11972 -0.47984,0.11972 -0.69932,0 l -6.03584,-3.59126 c -0.17957,-0.10535 -0.42494,-0.0974 -0.59854,0 -1.6661,0.94451 -1.97855,1.05459 -3.54088,1.59653 -0.38509,0.13408 -0.97015,0.34304 0.19911,0.99798 l 7.88185,4.63841 c 0.75024,0.43418 1.58535,0.64894 2.44333,0.64894 0.86996,0 1.69435,-0.21476 2.44458,-0.64894 l 23.49564,-13.51825 c 1.52044,-0.88434 2.44333,-2.53044 2.44333,-4.28936 l 0,-27.0869 c 0,-1.75988 -0.92289,-3.41153 -2.44333,-4.29059 l -23.49564,-13.56741 c -0.73627,-0.42014 -1.58159,-0.59855 -2.44458,-0.59855 z m 6.28534,19.35499 c -6.69034,0 -10.67425,2.84698 -10.67425,7.58196 0,5.13662 3.95462,6.54723 10.37559,7.18254 7.68201,0.752 8.28007,1.88002 8.28007,3.39216 0,2.62281 -2.09017,3.7412 -7.03259,3.7412 -6.20947,0 -7.57488,-1.54948 -8.03181,-4.6384 -0.0538,-0.33123 -0.30973,-0.59854 -0.64893,-0.59854 l -3.04188,0 c -0.37512,0 -0.69932,0.32232 -0.69932,0.69809 0,3.95393 2.15198,8.63034 12.42194,8.63034 7.43458,0 11.72261,-2.91881 11.72261,-8.0318 0,-5.06877 -3.46436,-6.42942 -10.67547,-7.38286 -7.28694,-0.96414 -7.98141,-1.43611 -7.98141,-3.14267 0,-1.4087 0.58808,-3.2926 5.98545,-3.2926 4.8207,0 6.60341,1.04011 7.3337,4.29058 0.0639,0.30553 0.33245,0.54816 0.64771,0.54816 l 3.0431,0 c 0.18756,0 0.3693,-0.11493 0.49899,-0.2495 0.1277,-0.14365 0.21708,-0.30462 0.19911,-0.49776 -0.47089,-5.59793 -4.20423,-8.2309 -11.72261,-8.2309 z"
675- id="path4116"
676- inkscape:connector-curvature="0" />
677- <path
678- style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
679- d="m -193.22175,651.29076 0,0.83723 2.33334,0 0,6.16277 0.95662,0 0,-6.16277 2.39377,0 0,-0.83723 -5.68373,0 z m 6.52096,0 0,7 0.89766,0 0,-4.12866 c 0,-0.16846 0.008,-0.42818 0,-0.77679 -0.0115,-0.35513 0,-0.66564 0,-0.83871 l 0,-0.17982 1.97368,5.92251 0.95662,0 2.03411,-5.92251 c 0,0.37675 -0.0496,0.74113 -0.0589,1.07748 -0.004,0.32736 0,0.57311 0,0.71784 l 0,4.12719 0.89766,0 0,-6.99853 -1.31628,0 -2.03411,5.92251 -1.97515,-5.92251 -1.37523,0 z"
680- id="path4114"
681- inkscape:connector-curvature="0" />
682 </g>
683 <g
684 inkscape:groupmode="layer"
685 id="layer3"
686 inkscape:label="PLACE YOUR PICTOGRAM HERE"
687- style="display:inline" />
688+ style="display:inline">
689+ <path
690+ inkscape:connector-curvature="0"
691+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
692+ id="path28"
693+ d="m 47.833258,16.541698 c -0.870556,0 -1.716941,0.1807 -2.465712,0.606108 L 21.666509,30.886221 c -1.533772,0.88637 -2.465712,2.561812 -2.465712,4.343768 l 0,27.426403 c 0,1.780987 0.93194,3.454484 2.465712,4.343608 l 6.189443,3.586212 c 3.007161,1.487625 4.116233,1.464674 5.484953,1.464674 4.452374,0 6.994572,-2.711642 6.994572,-7.424728 l 0,-27.07276 c 0,-0.382737 -0.330104,-0.656698 -0.704489,-0.656698 l -2.968916,0 c -0.380429,0 -0.704496,0.273961 -0.704496,0.656698 l 0,27.07276 c 0,2.089049 -2.167811,4.189899 -5.686232,2.424426 l -6.441042,-3.788085 c -0.227451,-0.12445 -0.40257,-0.344106 -0.40257,-0.606107 l 0,-27.426403 c 0,-0.260384 0.173107,-0.524807 0.40257,-0.656534 L 47.481013,20.885466 c 0.221408,-0.127688 0.485087,-0.127688 0.704489,0 l 23.650711,13.687989 c 0.225439,0.135767 0.402564,0.38823 0.402564,0.656534 l 0,27.426403 c 0,0.262001 -0.130912,0.530305 -0.352245,0.656539 l -23.70103,13.68799 c -0.203297,0.121221 -0.483081,0.121221 -0.704489,0 L 41.39221,73.364278 c -0.181156,-0.106675 -0.42873,-0.0986 -0.60385,0 -1.680706,0.956357 -1.996723,1.067554 -3.572764,1.616285 -0.388473,0.135768 -0.978236,0.347019 0.201284,1.010179 l 7.950666,4.697247 c 0.756821,0.439624 1.600195,0.656534 2.465712,0.656534 0.877589,0 1.708891,-0.21691 2.465711,-0.656534 L 74,67 c 1.533777,-0.895425 2.465712,-2.562621 2.465712,-4.343608 l 0,-27.426403 c 0,-1.781956 -0.931935,-3.453676 -2.465712,-4.343768 L 50.298969,17.147806 C 49.556234,16.722398 48.703813,16.541698 47.833258,16.541698 z M 54.17366,36.13947 c -6.74901,0 -10.768622,2.882968 -10.768622,7.677349 0,5.201047 3.989422,6.630001 10.466697,7.273281 7.749386,0.761436 8.353229,1.903501 8.353229,3.434606 0,2.655717 -2.109443,3.788084 -7.09521,3.788084 -6.263916,0 -7.640692,-1.56957 -8.101631,-4.697241 -0.05428,-0.335384 -0.311987,-0.606108 -0.654164,-0.606108 l -3.069561,0 c -0.37841,0 -0.704489,0.326648 -0.704489,0.707125 0,4.003534 2.169824,8.738119 12.529845,8.738119 7.499794,0 11.825355,-2.954892 11.825355,-8.132011 0,-5.132353 -3.494268,-6.509914 -10.768621,-7.47532 -7.350847,-0.976236 -8.051307,-1.454007 -8.051307,-3.181975 0,-1.426374 0.593782,-3.333749 6.038479,-3.333749 4.862987,0 6.660444,1.052684 7.397136,4.343927 0.06441,0.309356 0.336146,0.555511 0.654168,0.555511 l 3.069561,0 c 0.189206,0 0.372374,-0.116372 0.503211,-0.252622 0.128817,-0.145465 0.219395,-0.309357 0.201279,-0.504927 C 65.523989,38.805369 61.757989,36.13947 54.17366,36.13947 z"
694+ sodipodi:nodetypes="sccssccsssssssccssccccssccccccccsccssccssscsscssssscsscssccs" />
695+ </g>
696 <g
697 inkscape:groupmode="layer"
698 id="layer2"
699@@ -282,7 +222,7 @@
700 style="opacity:0.6;filter:url(#filter891)">
701 <path
702 transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
703- d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z"
704+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
705 sodipodi:ry="12"
706 sodipodi:rx="12"
707 sodipodi:cy="552.36218"
708@@ -301,11 +241,11 @@
709 sodipodi:cy="552.36218"
710 sodipodi:rx="12"
711 sodipodi:ry="12"
712- d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z"
713+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
714 transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
715 <path
716 transform="matrix(1.25,0,0,1.25,33,-100.45273)"
717- d="m 264,552.36218 c 0,6.62742 -5.37258,12 -12,12 -6.62742,0 -12,-5.37258 -12,-12 0,-6.62741 5.37258,-12 12,-12 6.62742,0 12,5.37259 12,12 z"
718+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 12,12 0 1 1 24,0 z"
719 sodipodi:ry="12"
720 sodipodi:rx="12"
721 sodipodi:cy="552.36218"
722
723=== added directory 'lib'
724=== added file 'lib/app.js'
725--- lib/app.js 1970-01-01 00:00:00 +0000
726+++ lib/app.js 2014-01-24 18:05:39 +0000
727@@ -0,0 +1,435 @@
728+var execFile = require('child_process').execFile;
729+var util = require('util');
730+var vm = require('vm');
731+var fs = require('fs');
732+var path = require('path');
733+var uidNumber = require('uid-number');
734+var mustache = require('mustache');
735+var vasync = require('vasync');
736+var charm = require('./charm')();
737+
738+module.exports = App;
739+
740+function App(options) {
741+ if (!(this instanceof App)) return new App(options);
742+
743+ if (options == null) {
744+ options = {};
745+ }
746+
747+ // assuming charm.js is in lib/charm.js the config file defaults to config/config.js
748+ this.CONFIG_FILE = options.config || path.join(__dirname, '..', 'config', 'config.js');
749+}
750+
751+function loadTemplate(template) {
752+ return fs.readFileSync(path.join(__dirname, '..', 'tpl', template)) + '';
753+}
754+
755+App.prototype.exec = function(cmd, args, cb) {
756+ var self = this;
757+ execFile(cmd, args, function(err, stdout, stderr) {
758+ if (err) {
759+ charm.log(util.format('failed to run: %s %j %j', cmd, args, stderr));
760+ process.exit(1);
761+ return;
762+ }
763+ if (typeof cb != 'undefined') {
764+ cb(stdout ? stdout.trim() : '');
765+ }
766+ });
767+};
768+
769+App.prototype.configUpdate = function(section, value, cb) {
770+ var self = this;
771+ fs.readFile(this.CONFIG_FILE, 'utf8', function(err, value) {
772+ if (err) {
773+ charm.log(util.format('Failed to read config file %s, trying to create it...', this.CONFIG_FILE));
774+ // anything else to do? will writefile create file?
775+ }
776+ var config = vm.runInNewContext('(function(){ return ' + value + ' })()');
777+ var s = config[section] || {};
778+ util._extend(s, value);
779+ config[section] = s;
780+ // write back to the config file
781+ fs.writeFile(this.CONFIG_FILE, config, function(err) {
782+ if (err) {
783+ charm.log(util.format('Failed to write config file %s: %j', this.CONFIG_FILE, err));
784+ process.exit(1);
785+ return;
786+ }
787+ })
788+ });
789+};
790+
791+App.prototype.installPackage = function(pkg, cb) {
792+ var self = this;
793+ // check if already installed
794+ self.exec('apt-cache',['policy', pkg], function(resp){
795+ // if not installed, install package
796+ if (resp.indexOf('Installed: (none)') != -1) {
797+ charm.log(util.format('Installing %s...', pkg));
798+ self.exec('apt-get', ['-y', 'install', '-qq', pkg], function(err) {
799+ if (err) {
800+ charm.log(util.format('Failed to install %s: %j', pkg, err));
801+ process.exit(1);
802+ return;
803+ }
804+ });
805+ }
806+ else {
807+ charm.log(util.format('%s is already installed, nothing to do.', pkg));
808+ }
809+ });
810+ cb();
811+}
812+
813+App.prototype.installApp = function(config, cb) {
814+ var self = this;
815+ var appDir = config.install_root + '/' + config.app_name;
816+ // check if already installed
817+ fs.exists(appDir, function(exists) {
818+ if(!exists) {
819+ // install from repository
820+ var command = '';
821+ var args= [];
822+ switch(config.app_scm) {
823+ case 'subversion':
824+ command = 'svn';
825+ args.push('checkout', config.app_url, appDir);
826+ break;
827+ case 'mercurial':
828+ command = 'hg';
829+ args.push('clone', config.app_url, '-r' , config.app_branch, appDir);
830+ break;
831+ case 'bzr':
832+ command = 'bzr';
833+ args.push('checkout', config.app_url, '-r' , config.app_branch, appDir);
834+ break;
835+ default:
836+ command = 'git';
837+ args.push('clone', config.app_url, '-b' , config.app_branch, appDir);
838+ break;
839+ }
840+ self.exec(command, args, function() {
841+ uidNumber(config.app_user, config.app_user, function (err, uid, gid) {
842+ if (err) {
843+ charm.log(util.format('Failed to get uid:gid for %s:%s: %j', config.app_user, config.app_user, err));
844+ process.exit(1);
845+ return;
846+ }
847+ else {
848+ fs.chown(appDir, uid, gid, function(err) {
849+ if (err) {
850+ charm.log(util.format('Failed to change ownership of %s to %s: %j', appDir, config.app_user, err));
851+ process.exit(1);
852+ return;
853+ }
854+ else {
855+ vasync.pipeline({
856+ funcs: [
857+ function installNpmModules(args, cb) {
858+ var self = args[1];
859+ var appDir = args[0].install_root + '/' + args[0].app_name;
860+ // install module dependencies, if any
861+ var dependencies = appDir + '/package.json';
862+ fs.exists(dependencies, function (exists) {
863+ if (exists) {
864+ process.chdir(appDir);
865+ self.exec('npm', ['install']);
866+ }
867+ });
868+ cb();
869+ },
870+ function createAppService(args, cb) {
871+ var self = args[1];
872+ var appDir = args[0].install_root + '/' + args[0].app_name;
873+ var procFile = appDir + '/Procfile';
874+ fs.exists(procFile, function (exists) {
875+ if (exists) {
876+ self.exec('npm', ['install', 'foreman']);
877+ }
878+ else {
879+ var output = mustache.render(loadTemplate('upstart.tpl'), args[0]);
880+ var upstartFile = '/etc/init/' + args[0].app_name + '.conf'
881+ fs.writeFile(upstartFile, output, function (err) {
882+ if (err) {
883+ charm.log(util.format('Failed to write upstart file %s: %j', upstartFile, err));
884+ process.exit(1);
885+ return;
886+ }
887+ });
888+ }
889+ });
890+ cb();
891+ }
892+ ],
893+ arg: [
894+ config,
895+ self
896+ ]
897+ }, function(err) {
898+ if (err) {
899+ charm.log(util.format('An error occurred: %j', err));
900+ process.exit(1);
901+ return;
902+ }
903+ });
904+ }
905+ });
906+ }
907+ });
908+ });
909+ }
910+ });
911+ cb();
912+}
913+
914+App.prototype.updateApp = function(config, cb) {
915+ var self = this;
916+ var appDir = config.install_root + '/' + config.app_name;
917+
918+ process.chdir(appDir);
919+
920+ // update from repository
921+ var command = '';
922+ var args= [];
923+ switch(config.app_scm) {
924+ case 'subversion':
925+ command = 'svn';
926+ args.push('update')
927+ break;
928+ case 'mercurial':
929+ command = 'hg';
930+ args.push('merge')
931+ break;
932+ case 'bzr':
933+ command = 'bzr';
934+ args.push('merge')
935+ break;
936+ default:
937+ command = 'git';
938+ args.push('checkout', config.app_branch);
939+ break;
940+ };
941+ self.exec(command, args, function(err) {
942+ if (err) {
943+ charm.log(util.format('Failed to update %s from %s: %j', config.app_name, config.app_url, err));
944+ process.exit(1);
945+ return;
946+ }
947+ });
948+ cb();
949+}
950+
951+App.prototype.configureApp = function(config, cb) {
952+ var self = this;
953+ var configFile = path.join(__dirname, '..', 'config', 'config.js');
954+ var appDir = config.install_root + '/' + config.app_name;
955+ var mongodbConfigFile = '/etc/juju_nodejs_app_' + config.app_name + '_mongodb.env';
956+ var mongodbConfig = "";
957+
958+ charm.log(util.format('Configuring %s...', config.app_name));
959+
960+ var procFile = appDir + '/Procfile';
961+ fs.exists(procFile, function(exists) {
962+ if(exists) {
963+ charm.log(util.format('Using Procfile to configure %s using foreman...', config.app_name));
964+
965+ fs.exists(mongodbConfigFile, function(exists) {
966+ if(exists) {
967+ fs.readFile(mongodbConfigFile, function (err, data) {
968+ if (err) {
969+ charm.log(util.format('Failed to read config file %s: %j', mongodbConfigFile, err));
970+ process.exit(1);
971+ return;
972+ }
973+ mongodbConfig = data;
974+ });
975+ }
976+ else {
977+ var output = mustache.render(loadTemplate('mongodb_env.tpl'), config);
978+ fs.writeFile(mongodbConfigFile, output, function (err) {
979+ if (err) {
980+ charm.log(util.format('Failed to write config file %s: %j', mongodbConfigFile, err));
981+ process.exit(1);
982+ return;
983+ }
984+ });
985+ }
986+ });
987+
988+ process.chdir(appDir);
989+ var args = ['export', '-j', appDir + '/Procfile', '-e', mongodbConfigFile, '-a', config.app_name, '-u', config.app_user, '-p', config.app_port, '-o', '/etc/init', '-t', 'upstart'];
990+ self.exec('nf', args, function(err) {
991+ if (err) {
992+ charm.log(util.format('Failed to execute foreman: %j', err));
993+ process.exit(1);
994+ return;
995+ }
996+ });
997+ }
998+ else {
999+ charm.log(util.format('Writing %s config file %s', config.app_name, configFile));
1000+ fs.exists(configFile, function(exists) {
1001+ if(exists) {
1002+ var data = fs.readFileSync(configFile, {encoding:'utf8'});
1003+ if (data == null) {
1004+ charm.log(util.format('Couldn\'t read config file %s: %j', configFile, err));
1005+ process.exit(1);
1006+ return;
1007+ }
1008+
1009+ var result = data.replace(/name.*/g, 'name : "' + config.app_name + '"');
1010+ result = result.replace(/listen_port.*/g, 'listen_port : "' + config.app_port + '"');
1011+
1012+ fs.writeFile(configFile, result, 'utf8', function (err) {
1013+ if (err) {
1014+ charm.log(util.format('Couldn\'t write config file %s: %j', configFile, err));
1015+ process.exit(1);
1016+ return;
1017+ }
1018+ });
1019+ }
1020+ else {
1021+ charm.log(util.format('Config file %s doesn\'t exist!', configFile));
1022+ process.exit(1);
1023+ return;
1024+ }
1025+ });
1026+ }
1027+ });
1028+
1029+ // cron schedule???
1030+
1031+ vasync.pipeline({
1032+ funcs: [
1033+ function installNpmModules(args, cb) {
1034+ var self = args[1];
1035+ var appDir = args[0].install_root + '/' + args[0].app_name;
1036+
1037+ self.exec('npm', ['update'], function() {
1038+ self.exec('npm', ['install'], function() {
1039+ });
1040+ });
1041+ cb();
1042+ },
1043+ function chownAndRestart(args, cb) {
1044+ var self = args[1];
1045+ var config = args[0];
1046+ var appDir = args[0].install_root + '/' + args[0].app_name;
1047+
1048+ uidNumber(config.app_user, config.app_user, function (err, uid, gid) {
1049+ if (err) {
1050+ charm.log(util.format('Failed to get uid:gid for %s:%s: %j', config.app_user, config.app_user, err));
1051+ process.exit(1);
1052+ return;
1053+ }
1054+ else {
1055+ fs.chown(appDir, uid, gid, function(err) {
1056+ if (err) {
1057+ charm.log(util.format('Failed to change ownership of %s to %s: %j', appDir, config.app_user, err));
1058+ process.exit(1);
1059+ return;
1060+ }
1061+ else {
1062+ self.exec('service', [config.app_name, 'restart'], function(ret) {
1063+ if (ret < 0) {
1064+ charm.log(util.format('Failed to restart %s', config.app_name));
1065+ }
1066+ else {
1067+ charm.log(util.format('%s restarted.', config.app_name));
1068+ }
1069+ });
1070+ }
1071+ });
1072+ }
1073+ });
1074+ cb();
1075+ }
1076+ ],
1077+ arg: [
1078+ config,
1079+ self
1080+ ]
1081+ }, function(err) {
1082+ if (err) {
1083+ charm.log(util.format('An error occurred: %j', err));
1084+ process.exit(1);
1085+ return;
1086+ }
1087+ });
1088+
1089+ cb();
1090+}
1091+
1092+App.prototype.reconfigureApp = function(config, cb) {
1093+ var self = this;
1094+ var configFile = path.join(__dirname, '..', 'config', 'config.js');
1095+ var appDir = config.install_root + '/' + config.app_name;
1096+ var mongodbConfigFile = '/etc/juju_nodejs_app_' + config.app_name + '_mongodb.env';
1097+
1098+ self.relation_get(['--format', 'json'], function(relation) {
1099+ var mongoNodeRelation = JSON.parse(relation);
1100+ if (mongoNodeRelation.hostname != ''){
1101+ charm.log(util.format('Configuring %s to work with the mongodb service...', config.app_name));
1102+
1103+ var procFile = appDir + '/Procfile';
1104+ fs.exists(procFile, function(exists) {
1105+ if(exists) {
1106+ var output = mustache.render(loadTemplate('mongodb_env.tpl'), config);
1107+ fs.writeFile(mongodbConfigFile, output, function (err) {
1108+ if (err) {
1109+ charm.log(util.format('Failed to write config file %s: %j', mongodbConfigFile, err));
1110+ process.exit(1);
1111+ return;
1112+ }
1113+ });
1114+ self.emit('config-changed', config);
1115+ }
1116+ else {
1117+ charm.log(util.format('Writing %s config file %s', config.app_name, configFile));
1118+ fs.exists(configFile, function(exists) {
1119+ if(exists) {
1120+ var data = fs.readFileSync(configFile, {encoding:'utf8'});
1121+ if (data == null) {
1122+ charm.log(util.format('Couldn\'t read config file %s: %j', configFile, err));
1123+ process.exit(1);
1124+ return;
1125+ }
1126+
1127+ var result = data.replace(/mongo_host.*/g, 'mongo_host : "' + mongoNodeRelation.hostname + '"');
1128+ result = result.replace(/mongo_port.*/g, 'mongo_port : "' + mongoNodeRelation.port + '"');
1129+ result = result.replace(/mongo_replset.*/g, 'mongo_replset : "' + mongoNodeRelation.replset + '"');
1130+
1131+ fs.writeFile(configFile, result, 'utf8', function (err) {
1132+ if (err) {
1133+ charm.log(util.format('Couldn\'t write config file %s: %j', configFile, err));
1134+ process.exit(1);
1135+ return;
1136+ }
1137+ });
1138+ }
1139+ else {
1140+ charm.log(util.format('Config file %s doesn\'t exist!', configFile));
1141+ process.exit(1);
1142+ return;
1143+ }
1144+ });
1145+ }
1146+ });
1147+
1148+ charm.open_port([config.app_port + '/TCP']);
1149+
1150+ charm.log(util.format('Restarting %s...', config.app_name))
1151+ self.exec('service', [config.app_name, 'restart'], function(ret) {
1152+ if (ret < 0) {
1153+ charm.log(util.format('Failed to restart %s', config.app_name));
1154+ }
1155+ else {
1156+ charm.log(util.format('%s restarted.', config.app_name));
1157+ }
1158+ });
1159+ }
1160+ });
1161+ cb();
1162+}
1163
1164=== added file 'lib/charm.js'
1165--- lib/charm.js 1970-01-01 00:00:00 +0000
1166+++ lib/charm.js 2014-01-24 18:05:39 +0000
1167@@ -0,0 +1,75 @@
1168+var execFile = require('child_process').execFile;
1169+var EE = require('events').EventEmitter;
1170+var util = require('util');
1171+
1172+module.exports = Charm;
1173+
1174+function Charm() {
1175+ if (!(this instanceof Charm)) return new Charm();
1176+
1177+ // delay a tick so things can attach, maybe have a separate init that users pass in instead of this being magic on first require
1178+ var self = this;
1179+ process.nextTick(function() {
1180+ //argv[0] is node, argv[1] is script name, the rest will be args for the hook
1181+ var hook = process.argv[1];
1182+ //if we have defined a prototype method for this call it instead. otherwise re-emit
1183+ if (self[hook])
1184+ self[hook].apply(self, process.argv.slice(2));
1185+ else
1186+ self.emit.apply(self, process.argv.slice(1));
1187+ });
1188+}
1189+util.inherits(Charm, EE);
1190+
1191+Charm.prototype.relation_get = function(key, cb) {
1192+ this.exec('relation-get', key, cb);
1193+};
1194+
1195+Charm.prototype.relation_set = function(map, cb) {
1196+ var params = [];
1197+ for (var key in map) {
1198+ params.push(key + '=' + map[key])
1199+ }
1200+ this.exec('relation-set', params, cb);
1201+};
1202+
1203+Charm.prototype.log = function(msg, cb) {
1204+ this.exec('juju-log', [msg], cb);
1205+};
1206+
1207+Charm.prototype.config_get = function(key, cb) {
1208+ this.exec('config-get', [key], cb);
1209+};
1210+
1211+Charm.prototype.config_get_all = function(cb) {
1212+ this.exec('config-get', ['--all', '--format', 'json'], function(config) {
1213+ cb(JSON.parse(config));
1214+ });
1215+};
1216+
1217+Charm.prototype.unit_get = function(key, cb) {
1218+ this.exec('unit-get', [key], cb);
1219+};
1220+
1221+Charm.prototype.open_port = function(key, cb) {
1222+ this.exec('open-port', [key], cb);
1223+};
1224+
1225+Charm.prototype.close_port = function(key, cb) {
1226+ this.exec('close-port', [key], cb);
1227+};
1228+
1229+/* evil this exits like set -e does */
1230+Charm.prototype.exec = function(cmd, args, cb) {
1231+ var self = this;
1232+ execFile(cmd, args, function(err, stdout, stderr) {
1233+ if (err) {
1234+ self.log(util.format('failed to run: %s %j %j', cmd, args, stderr));
1235+ process.exit(1);
1236+ return;
1237+ }
1238+ if (typeof cb != 'undefined') {
1239+ cb(stdout ? stdout.trim() : '');
1240+ }
1241+ });
1242+};
1243\ No newline at end of file
1244
1245=== modified file 'metadata.yaml'
1246--- metadata.yaml 2013-07-11 19:24:26 +0000
1247+++ metadata.yaml 2014-01-24 18:05:39 +0000
1248@@ -3,7 +3,7 @@
1249 description: "Deploy a user-defined node.js application"
1250 categories:
1251 - app-servers
1252-maintainer: Mark Mims <mark.mims@canonical.com>
1253+maintainer: Daniele Stroppa <daniele.stroppa@joyent.com>
1254 requires:
1255 mongodb:
1256 interface: mongodb
1257
1258=== added directory 'tests'
1259=== added file 'tests/00-setup'
1260--- tests/00-setup 1970-01-01 00:00:00 +0000
1261+++ tests/00-setup 2014-01-24 18:05:39 +0000
1262@@ -0,0 +1,5 @@
1263+#!/bin/bash
1264+
1265+add-apt-repository ppa:juju/stable
1266+apt-get update
1267+apt-get install amulet
1268
1269=== added file 'tests/01-mongodb-deployment'
1270--- tests/01-mongodb-deployment 1970-01-01 00:00:00 +0000
1271+++ tests/01-mongodb-deployment 2014-01-24 18:05:39 +0000
1272@@ -0,0 +1,31 @@
1273+#!/usr/bin/python3
1274+
1275+import amulet
1276+
1277+d = amulet.Deployment()
1278+
1279+d.add('node-app', charm='lp:~dstroppa/charms/precise/node-app')
1280+d.add('mongodb', charm='cs:precise/mongodb-20')
1281+
1282+d.relate('node-app:mongodb', 'mongodb:database')
1283+
1284+d.expose('node-app')
1285+
1286+try:
1287+ d.setup(timeout=900)
1288+ d.sentry.wait()
1289+except amulet.helpers.TimeoutError:
1290+ amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
1291+except:
1292+ raise
1293+
1294+# Now you can use d.sentry.unit[UNIT] to address each of the units and perform
1295+# more in-depth steps. There are three test statuses: amulet.PASS, amulet.FAIL,
1296+# and amulet.SKIP - these can be triggered with amulet.raise_status(). Each
1297+# d.sentry.unit[] has the following methods:
1298+# - .info - An array of the information of that unit from Juju
1299+# - .file(PATH) - Get the details of a file on that unit
1300+# - .file_contents(PATH) - Get plain text output of PATH file from that unit
1301+# - .directory(PATH) - Get details of directory
1302+# - .directory_contents(PATH) - List files and folders in PATH on that unit
1303+# - .relation(relation, service:rel) - Get relation data from return service
1304
1305=== added file 'tests/02-mongodb-haproxy-deployment'
1306--- tests/02-mongodb-haproxy-deployment 1970-01-01 00:00:00 +0000
1307+++ tests/02-mongodb-haproxy-deployment 2014-01-24 18:05:39 +0000
1308@@ -0,0 +1,33 @@
1309+#!/usr/bin/python3
1310+
1311+import amulet
1312+
1313+d = amulet.Deployment()
1314+
1315+d.add('node-app', charm='lp:~dstroppa/charms/precise/node-app')
1316+d.add('haproxy', charm='cs:precise/haproxy-22')
1317+d.add('mongodb', charm='cs:precise/mongodb-20')
1318+
1319+d.relate('node-app:website', 'haproxy:reverseproxy')
1320+d.relate('node-app:mongodb', 'mongodb:database')
1321+
1322+d.expose('haproxy')
1323+
1324+try:
1325+ d.setup(timeout=900)
1326+ d.sentry.wait()
1327+except amulet.helpers.TimeoutError:
1328+ amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
1329+except:
1330+ raise
1331+
1332+# Now you can use d.sentry.unit[UNIT] to address each of the units and perform
1333+# more in-depth steps. There are three test statuses: amulet.PASS, amulet.FAIL,
1334+# and amulet.SKIP - these can be triggered with amulet.raise_status(). Each
1335+# d.sentry.unit[] has the following methods:
1336+# - .info - An array of the information of that unit from Juju
1337+# - .file(PATH) - Get the details of a file on that unit
1338+# - .file_contents(PATH) - Get plain text output of PATH file from that unit
1339+# - .directory(PATH) - Get details of directory
1340+# - .directory_contents(PATH) - List files and folders in PATH on that unit
1341+# - .relation(relation, service:rel) - Get relation data from return service
1342
1343=== added directory 'tpl'
1344=== added file 'tpl/mongodb_env.tpl'
1345--- tpl/mongodb_env.tpl 1970-01-01 00:00:00 +0000
1346+++ tpl/mongodb_env.tpl 2014-01-24 18:05:39 +0000
1347@@ -0,0 +1,8 @@
1348+{
1349+ "name": "{{appName}}"
1350+ , "node_env": "{{appNodeEnv}}"
1351+ , "mongo": {{mongdbConfig}}
1352+ , "host": "{{hostname}}"
1353+ , "port": {{port}}
1354+ , "replset": "{{replset}}"
1355+}
1356\ No newline at end of file
1357
1358=== added file 'tpl/upstart.tpl'
1359--- tpl/upstart.tpl 1970-01-01 00:00:00 +0000
1360+++ tpl/upstart.tpl 2014-01-24 18:05:39 +0000
1361@@ -0,0 +1,16 @@
1362+description "{{app_name}} node.js server"
1363+author "Daniele Stroppa <daniele.stroppa@joyent.com>"
1364+
1365+start on (local-filesystems and net-device-up IFACE!=lo)
1366+stop on runlevel [06]
1367+
1368+respawn
1369+respawn limit 5 3
1370+
1371+setuid {{app_user}}
1372+setgid {{app_user}}
1373+
1374+env HOME="/home/{{app_user}}"
1375+env NODE_PATH="/usr/lib/node"
1376+
1377+exec /usr/bin/node {{{install_root}}}/{{app_name}}/server.js >> /var/log/{{app_name}}.log 2>&1

Subscribers

People subscribed via source and target branches