1 | #!/usr/bin/env node
|
2 | 'use strict';
|
3 |
|
4 |
|
5 |
|
6 | var child_process = require('child_process');
|
7 | var fs = require('fs');
|
8 | var net = require('net');
|
9 | var DDPClient = require('ddp');
|
10 | var login = require('ddp-login');
|
11 | var netrc = require('netrc');
|
12 |
|
13 | var help = 'Usage: skale [options] <command> [<args>]\n' +
|
14 | '\n' +
|
15 | 'Create, test, deploy, run clustered NodeJS applications\n' +
|
16 | '\n' +
|
17 | 'Commands:\n' +
|
18 | ' create <app> Create a new application\n' +
|
19 | ' test [<args>...] Run application on local host\n' +
|
20 | ' deploy [<args>...] Deploy application on skale cloud\n' +
|
21 | ' run [<args>...] Run application on skale cloud\n' +
|
22 | ' attach Attach to a running application\n' +
|
23 | ' log Print log of an application\n' +
|
24 | ' signup Create an account on skale cloud\n' +
|
25 | ' status Print status of application on skale cloud\n' +
|
26 | ' stop Stop application on skale cloud\n' +
|
27 | '\n' +
|
28 | 'Options:\n' +
|
29 | ' -d, --debug Enable debug traces\n' +
|
30 | ' -f, --file Set program to run (default: package name)\n' +
|
31 | ' --force Force action to occur, despite warning\n' +
|
32 | ' -h, --help Print help and quit\n' +
|
33 | ' -m, --memory MB Set the memory space limit per worker (default 4000 MB)\n' +
|
34 | ' -r, --remote Run in the cloud instead of locally\n' +
|
35 | ' -V, --version Print version and quit\n' +
|
36 | ' -w, --worker num Set the number of workers (default 2)\n';
|
37 |
|
38 | var argv = require('minimist')(process.argv.slice(2), {
|
39 | string: [
|
40 | 'c', 'config',
|
41 | 'f', 'file',
|
42 | 'm', 'memory',
|
43 | 'w', 'worker',
|
44 | ],
|
45 | boolean: [
|
46 | 'd', 'debug',
|
47 | 'force',
|
48 | 'h', 'help',
|
49 | 'V', 'version',
|
50 | ],
|
51 | default: {}
|
52 | });
|
53 |
|
54 |
|
55 | if (argv.h || argv.help) {
|
56 | console.log(help);
|
57 | process.exit();
|
58 | }
|
59 | if (argv.V || argv.version) {
|
60 | var pkg = require('./package');
|
61 | console.log(pkg.name + '-' + pkg.version);
|
62 | process.exit();
|
63 | }
|
64 | if (argv.d || argv.debug) {
|
65 | process.env.SKALE_DEBUG = 3;
|
66 | }
|
67 |
|
68 | var configPath = argv.c || argv.config || process.env.SKALE_CONFIG || process.env.HOME + '/.skalerc';
|
69 | var config = load_config(argv);
|
70 | var proto = config.ssl ? require('https') : require('http');
|
71 | var memory = argv.m || argv.memory || 4000;
|
72 | var worker = argv.w || argv.worker || 2;
|
73 | var rc = netrc();
|
74 |
|
75 | switch (argv._[0]) {
|
76 | case 'attach':
|
77 | attach();
|
78 | break;
|
79 | case 'create':
|
80 | create(argv._[1]);
|
81 | break;
|
82 | case 'deploy':
|
83 | deploy(argv._.splice(1));
|
84 | break;
|
85 | case 'log':
|
86 | console.log('log: not implemented yet');
|
87 | break;
|
88 | case 'run':
|
89 | run_remote(argv._.splice(1));
|
90 | break;
|
91 | case 'signup':
|
92 | console.log('signup: not implemented yet');
|
93 | break;
|
94 | case 'status':
|
95 | status();
|
96 | break;
|
97 | case 'stop':
|
98 | stop();
|
99 | break;
|
100 | case 'test':
|
101 | run_local(argv._.splice(1));
|
102 | break;
|
103 | default:
|
104 | die('Error: invalid command: ' + argv._[0]);
|
105 | }
|
106 |
|
107 | function checkName(name) {
|
108 | return /^[A-Za-z][A-Za-z0-9_-]+$/.test(name);
|
109 | }
|
110 |
|
111 | function create(name) {
|
112 | if (!checkName(name)) die('skale create error: invalid name ' + name);
|
113 | console.log('create application ' + name);
|
114 | try {
|
115 | fs.mkdirSync(name);
|
116 | } catch (error) {
|
117 | die('skale create error: ' + error.message);
|
118 | }
|
119 | process.chdir(name);
|
120 | console.log('create local repository');
|
121 | child_process.execSync('git init');
|
122 |
|
123 | var pkg = {
|
124 | name: name,
|
125 | version: '0.1.0',
|
126 | private: true,
|
127 | keywords: [ 'skale' ],
|
128 | dependencies: {
|
129 | 'skale-engine': '^0.6.1'
|
130 | }
|
131 | };
|
132 | fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
133 | var src = '#!/usr/bin/env node\n' +
|
134 | '\n' +
|
135 | 'var sc = require(\'skale-engine\').context();\n' +
|
136 | '\n' +
|
137 | 'sc.parallelize([\'Hello world\']).collect().then(function (res) {\n' +
|
138 | ' console.log(sc.worker.length + \' workers, res:\', res);\n' +
|
139 | ' sc.end();\n' +
|
140 | '});\n';
|
141 | fs.writeFileSync(name + '.js', src);
|
142 | var gitIgnore = 'node_modules\nnpm-debug.log*\n.npm-install-changed.json\n';
|
143 | fs.writeFileSync('.gitignore', gitIgnore);
|
144 | var npm = child_process.spawnSync('npm', ['install'], {stdio: 'inherit'});
|
145 | if (npm.status) die('skale create error: npm install failed');
|
146 | console.log('Project ' + name + ' is now ready.\n' +
|
147 | 'Please change directory to ' + name + ': "cd ' + name + '"\n' +
|
148 | 'To run your app locally: "skale test"\n' +
|
149 | 'To modify your app: edit ' + name + '.js');
|
150 | }
|
151 |
|
152 | function die(err) {
|
153 | console.error(err);
|
154 | process.exit(1);
|
155 | }
|
156 |
|
157 | function load_config(argv) {
|
158 | var conf = {}, save = false;
|
159 | try { conf = JSON.parse(fs.readFileSync(configPath)); } catch (error) { save = true; }
|
160 | process.env.SKALE_TOKEN = process.env.SKALE_TOKEN || conf.token;
|
161 | if (save || argv._[0] == 'init') save_config(conf);
|
162 | return conf;
|
163 | }
|
164 |
|
165 | function save_config(config) {
|
166 | fs.writeFile(configPath, JSON.stringify(config, null, 2), function (err) {
|
167 | if (err) die('Could node write ' + configPath + ':', err);
|
168 | });
|
169 | }
|
170 |
|
171 | function status_local() {
|
172 | var child = child_process.execFile('/bin/ps', ['ux'], function (err, out) {
|
173 | var lines = out.split(/\r\n|\r|\n/);
|
174 | for (var i = 0; i < lines.length; i++)
|
175 | if (i == 0 || lines[i].match(/ skale-/)) console.log(lines[i].trim());
|
176 | });
|
177 | }
|
178 |
|
179 | function run_local(args) {
|
180 | var pkg = JSON.parse(fs.readFileSync('package.json'));
|
181 | var cmd = argv.f || argv.file || pkg.name + '.js';
|
182 | args.splice(0, 0, cmd);
|
183 | child_process.spawn('node', args, {stdio: 'inherit'});
|
184 | }
|
185 |
|
186 | function skale_session(callback) {
|
187 | var host = process.env.SKALE_SERVER || 'apps.skale.me';
|
188 | var port = process.env.SKALE_PORT || 443;
|
189 |
|
190 | var ddp = new DDPClient({
|
191 |
|
192 | host : host,
|
193 | port : port,
|
194 | ssl : !process.env.SKALE_NOSSL,
|
195 | autoReconnect : true,
|
196 | autoReconnectTimer: 500,
|
197 | maintainCollections : true,
|
198 | ddpVersion: '1',
|
199 | useSockJs: true,
|
200 | url: 'wss://example.com/websocket'
|
201 | });
|
202 |
|
203 | ddp.connect(function (err, isreconnect) {
|
204 | if (err) return callback(err, ddp, isreconnect);
|
205 | login(ddp, {env: 'SKALE_TOKEN', retry: 2}, function (err, userInfo) {
|
206 | if (err) return callback(err, ddp, isreconnect);
|
207 | var token = userInfo.token;
|
208 | if (userInfo.token != config.token) {
|
209 | config.token = userInfo.token;
|
210 | save_config(config);
|
211 | }
|
212 | return callback(err, ddp, isreconnect);
|
213 | });
|
214 | });
|
215 | }
|
216 |
|
217 | function deploy(args) {
|
218 | skale_session(function (err, ddp, isreconnect) {
|
219 | if (err) {
|
220 | switch (err.reason) {
|
221 | case 'User not found':
|
222 | die('User not found');
|
223 | default:
|
224 | die(err.toString());
|
225 | }
|
226 | }
|
227 | var pkg = JSON.parse(fs.readFileSync('package.json'));
|
228 | var name = pkg.name;
|
229 |
|
230 | ddp.call('etls.add', [{name: name}], function (err, res) {
|
231 | if (err) die('Could not create application ' + name + ':', err);
|
232 | var a = res.url.split('/');
|
233 | var login = a[a.length - 2];
|
234 | var host = a[2].replace(/:.*/, '');
|
235 | var passwd = res.token;
|
236 | rc[host] = {login: login, password: passwd};
|
237 | netrc.save(rc);
|
238 | child_process.exec('git remote remove skale; git remote add skale "' + res.url + '"; git add -A .; git commit -m "automatic commit"; git push skale master', function (err, stdout, stderr) {
|
239 | if (err) die('deploy error: ' + err);
|
240 | ddp.call('etls.deploy', [{name: name}], function (err, res) {
|
241 | if (err) console.error(err);
|
242 | else console.log(name + ' deployed');
|
243 | ddp.close();
|
244 | });
|
245 | });
|
246 | });
|
247 | });
|
248 | }
|
249 |
|
250 | function run_remote(args) {
|
251 | var diff = child_process.execSync('git diff skale/master');
|
252 | if (diff.length) {
|
253 | if (argv.force) console.error('Warning, running an obsolete version, you should deploy');
|
254 | else die('Error: content has changed, deploy first or run --force');
|
255 | }
|
256 | skale_session(function (err, ddp, isreconnect) {
|
257 | if (err) die('Could not connect:', err);
|
258 | var pkg = JSON.parse(fs.readFileSync('package.json'));
|
259 | var name = pkg.name;
|
260 | var opt = {debug: process.env.SKALE_DEBUG};
|
261 |
|
262 | ddp.call('etls.run', [{name: name, opt: opt}], function (err, res) {
|
263 | if (err) die('run error:', err);
|
264 | if (res.alreadyStarted) die('Error: application is already running, use "skale attach" or "skale stop"');
|
265 | var taskId = res.taskId;
|
266 | ddp.subscribe('task.withTaskId', [taskId], function () {});
|
267 |
|
268 | var observer = ddp.observe('tasks');
|
269 | observer.changed = function (id, oldFields, clearedFields, newFields) {
|
270 | if (newFields.status && newFields.status != 'pending') ddp.close();
|
271 | if (newFields.out) {
|
272 | var olen = oldFields.out ? oldFields.out.length : 0;
|
273 | var nlen = newFields.out.length;
|
274 | for (var i = olen; i < nlen; i++) process.stdout.write(newFields.out[i] + '\n');
|
275 | }
|
276 | };
|
277 | });
|
278 | });
|
279 | }
|
280 |
|
281 | function attach() {
|
282 | skale_session(function (err, ddp, isreconnect) {
|
283 | if (err) die('Could not connect:', err);
|
284 | var pkg = JSON.parse(fs.readFileSync('package.json'));
|
285 | var name = pkg.name;
|
286 | ddp.subscribe('etls.withName', [name], function (err) {
|
287 | var etl = ddp.collections.etls[Object.keys(ddp.collections.etls)[0]];
|
288 | if (!etl.running) die('Application is not running, use "skale log" or "skale run"');
|
289 |
|
290 | ddp.subscribe('task.withTaskId', [etl.taskId], function () {
|
291 | var task = ddp.collections.tasks[Object.keys(ddp.collections.tasks)[0]];
|
292 | for (var i = 0; i < task.out.length; i++)
|
293 | console.log(task.out[i]);
|
294 | });
|
295 |
|
296 | var observer = ddp.observe('tasks');
|
297 | observer.changed = function (id, oldFields, clearedFields, newFields) {
|
298 | if (newFields.status && newFields.status != 'pending') ddp.close();
|
299 | if (newFields.out) {
|
300 | var olen = oldFields.out ? oldFields.out.length : 0;
|
301 | var nlen = newFields.out.length;
|
302 | for (var i = olen; i < nlen; i++) process.stdout.write(newFields.out[i] + '\n');
|
303 | }
|
304 | };
|
305 |
|
306 | });
|
307 | });
|
308 | }
|
309 |
|
310 | function status() {
|
311 | skale_session(function (err, ddp, isreconnect) {
|
312 | if (err) die('could node connect:', err);
|
313 | var pkg = JSON.parse(fs.readFileSync('package.json'));
|
314 | var name = pkg.name;
|
315 | ddp.subscribe('etls.withName', [name], function (err, data) {
|
316 | if (!ddp.collections.etls) die('etl not found:', name);
|
317 | var etl = ddp.collections.etls[Object.keys(ddp.collections.etls)[0]];
|
318 | console.log(etl.name, 'status:', etl.running ? 'running' : 'exited');
|
319 | ddp.close();
|
320 | });
|
321 | });
|
322 | }
|
323 |
|
324 | function stop() {
|
325 | skale_session(function (err, ddp, isreconnect) {
|
326 | if (err) die('could node connect:', err);
|
327 | var pkg = JSON.parse(fs.readFileSync('package.json'));
|
328 | var name = pkg.name;
|
329 | ddp.call('etls.reset', [{name: name}], function (err, res) {
|
330 | ddp.close();
|
331 | });
|
332 | });
|
333 | }
|