1 | var nodemiral = require('nodemiral');
|
2 | var path = require('path');
|
3 | var fs = require('fs');
|
4 | var rimraf = require('rimraf');
|
5 | var exec = require('child_process').exec;
|
6 | var spawn = require('child_process').spawn;
|
7 | var uuid = require('uuid');
|
8 | var format = require('util').format;
|
9 | var extend = require('util')._extend;
|
10 | var _ = require('underscore');
|
11 | var async = require('async');
|
12 | var buildApp = require('./build.js');
|
13 | var buildAppNextJS = require('./buildNextJS.js');
|
14 | var request = require('request');
|
15 | var pkg = require('../package.json');
|
16 |
|
17 | require('colors');
|
18 |
|
19 | var NEXTJS_DIR_DEFAULT_EXCLUSIONS = ['node_modules', '__test__', '__tests__'];
|
20 |
|
21 | module.exports = Actions;
|
22 |
|
23 | function Actions(config, cwd, options) {
|
24 | this.cwd = cwd;
|
25 | this.config = config;
|
26 | this.sessionsMap = this._createSessionsMap(config);
|
27 | this.settingsFileName = options.settingsFileName;
|
28 | this.configFileName = options.configFileName;
|
29 |
|
30 |
|
31 | var setttingsJsonPath = path.resolve(this.cwd, this.settingsFileName);
|
32 | if (fs.existsSync(setttingsJsonPath)) {
|
33 | this.config.env['METEOR_SETTINGS'] = JSON.stringify(require(setttingsJsonPath));
|
34 | }
|
35 | }
|
36 |
|
37 | Actions.prototype._createSessionsMap = function (config) {
|
38 | var sessionsMap = {};
|
39 |
|
40 | config.servers.forEach(function (server) {
|
41 | var host = server.host;
|
42 | var auth = {username: server.username};
|
43 |
|
44 | if (server.pem) {
|
45 | auth.pem = fs.readFileSync(path.resolve(server.pem), 'utf8');
|
46 | } else {
|
47 | auth.password = server.password;
|
48 | }
|
49 |
|
50 | var nodemiralOptions = {
|
51 | ssh: server.sshOptions,
|
52 | keepAlive: true
|
53 | };
|
54 |
|
55 | if (!sessionsMap[server.os]) {
|
56 | sessionsMap[server.os] = {
|
57 | sessions: [],
|
58 | taskListsBuilder: require('./taskLists')(server.os)
|
59 | };
|
60 | }
|
61 |
|
62 | var session = nodemiral.session(host, auth, nodemiralOptions);
|
63 | session._serverConfig = server;
|
64 | sessionsMap[server.os].sessions.push(session);
|
65 | });
|
66 |
|
67 | return sessionsMap;
|
68 | };
|
69 |
|
70 | Actions.prototype._executePararell = function (actionName, args) {
|
71 | var self = this;
|
72 | var sessionInfoList = _.values(self.sessionsMap);
|
73 | async.map(
|
74 | sessionInfoList,
|
75 | function (sessionsInfo, callback) {
|
76 | var taskList = sessionsInfo.taskListsBuilder[actionName]
|
77 | .apply(sessionsInfo.taskListsBuilder, args);
|
78 | taskList.run(sessionsInfo.sessions, function (summaryMap) {
|
79 | callback(null, summaryMap);
|
80 | });
|
81 | },
|
82 | whenAfterCompleted
|
83 | );
|
84 | };
|
85 |
|
86 | Actions.prototype.setup = function () {
|
87 | if(this.config.containersConfig === true) {
|
88 | var intVersion = parseInt(pkg.version.replace(/\./g, ''));
|
89 | if (!this.config.containerName) {
|
90 | console.error('Missing "containerName" property for setting up a container like instance'.red.bold);
|
91 | process.exit(1);
|
92 | }
|
93 | console.log('Setup of container-like server!! This may take a while... for sure...'.bold);
|
94 | var self = this;
|
95 | var serverHost = this.config.servers[0].host;
|
96 | var taskUrl = 'http://' + serverHost + ':12654/task';
|
97 | request({
|
98 | uri: taskUrl,
|
99 | method: 'POST',
|
100 | timeout: 1200000,
|
101 | json: {
|
102 | action: 'setupContainer',
|
103 | v: intVersion,
|
104 | pwd: self.config.APIPassword,
|
105 | options: {
|
106 | name: self.config.containerName,
|
107 | nodeVersion: self.config.nodeVersion,
|
108 | maxCPUQuota: self.config.maxCPUQuota,
|
109 | openToThisIPs: self.config.openToThisIPs,
|
110 | serverName: serverHost,
|
111 | }
|
112 | }
|
113 | }, function (err, res, body) {
|
114 | if(err) {
|
115 | console.log(('Error while calling: ' + taskUrl).red.bold);
|
116 | console.log('Error obj: '.red.bold, err);
|
117 | process.exit(1);
|
118 | }
|
119 | var containerResponse;
|
120 | var server = self.config.servers[0];
|
121 | if(typeof body === 'string') {
|
122 | containerResponse = JSON.parse(body);
|
123 | } else {
|
124 | containerResponse = body;
|
125 | }
|
126 | if(!server.password || !server.sshOptions || !server.sshOptions.port) {
|
127 | if(!containerResponse) {
|
128 | console.log(('No response from task: ' + taskUrl).red.bold);
|
129 | console.log('Full response object: '.red.bold, res);
|
130 | process.exit(1);
|
131 | }
|
132 | if (containerResponse.error === 'Unauthorized!') {
|
133 | console.log(('The password for the configuration API is not valid').red.bold);
|
134 | process.exit(1);
|
135 | }
|
136 | if (containerResponse.error) {
|
137 | console.log(containerResponse.error.red.bold);
|
138 | process.exit(1);
|
139 | }
|
140 | var port = parseInt(containerResponse._port);
|
141 | if(server.sshOptions) {
|
142 | server.sshOptions.port = port;
|
143 | } else {
|
144 | server.sshOptions = { port: port };
|
145 | }
|
146 | server.password = containerResponse._password;
|
147 | var sshAgent = process.env.SSH_AUTH_SOCK;
|
148 | if (sshAgent) {
|
149 | server.sshOptions.agent = sshAgent;
|
150 | }
|
151 | self.sessionsMap['linux'].sessions[0] = nodemiral.session(serverHost, {username: server.username, password: containerResponse._password}, { ssh: server.sshOptions, keepAlive: true });
|
152 | if (containerResponse._password) {
|
153 | var configFile = path.resolve(self.cwd, self.configFileName);
|
154 | var mupContent = fs.readFileSync(configFile, {encoding: 'utf8'});
|
155 | mupContent = mupContent.replace(/\/\/{{PASSWORD}}/g, '"password":"' + containerResponse._password + '",').replace(/\/\/{{SSH_OPTIONS}}/g, '"sshOptions":{"port":' + port + '},');
|
156 | fs.writeFileSync(configFile, mupContent);
|
157 | }
|
158 | }
|
159 | if (containerResponse.openMongoURL) {
|
160 | try {
|
161 | var settingsFile = path.resolve(self.cwd, self.settingsFileName);
|
162 | var settingsContent = fs.readFileSync(settingsFile, {encoding: 'utf8'});
|
163 | if (!/"openMongoURL"/.test(settingsContent)) {
|
164 | const privateRegExp = /(.*"private".*)/;
|
165 | if (privateRegExp.test(settingsContent)) {
|
166 | settingsContent = settingsContent.replace(/(.*"private".*)/, '$1\r\n "openMongoURL": "' + containerResponse.openMongoURL + '",');
|
167 | fs.writeFileSync(settingsFile, settingsContent);
|
168 | }
|
169 | }
|
170 | } catch (err) {
|
171 | console.log(err);
|
172 |
|
173 | }
|
174 | }
|
175 | request({
|
176 | uri: taskUrl,
|
177 | method: 'POST',
|
178 | timeout: 1200000,
|
179 | json: {
|
180 | action: 'setupNginx',
|
181 | v: intVersion,
|
182 | pwd: self.config.APIPassword,
|
183 | options: {
|
184 | name: self.config.containerName,
|
185 | port: self.config.env && self.config.env.PORT,
|
186 | host: ((self.config.env && self.config.env.ROOT_URL) || '').split('//').pop().replace(/\//g, '')
|
187 | }
|
188 | }
|
189 | }, function (err2, res2, body2) {
|
190 | if(err2) {
|
191 | console.log(('Error while calling: ' + taskUrl).red.bold);
|
192 | console.log('Error obj: '.red.bold, err2);
|
193 | process.exit(1);
|
194 | }
|
195 | var containerResponse;
|
196 | if(typeof body2 === 'string') {
|
197 | containerResponse = JSON.parse(body2);
|
198 | } else {
|
199 | containerResponse = body2;
|
200 | }
|
201 | if(!containerResponse) {
|
202 | console.log(('No response from task: ' + taskUrl).red.bold);
|
203 | console.log('Full response object: '.red.bold, res2);
|
204 | process.exit(1);
|
205 | }
|
206 | if (containerResponse.error === 'Unauthorized!') {
|
207 | console.log(('The password for the configuration API is not valid').red.bold);
|
208 | process.exit(1);
|
209 | }
|
210 | self._executePararell("setup", [self.config]);
|
211 | });
|
212 | });
|
213 | } else {
|
214 | this._executePararell("setup", [this.config]);
|
215 | }
|
216 | };
|
217 |
|
218 | Actions.prototype.deployMeteor = function () {
|
219 | var self = this;
|
220 | var buildLocation = path.resolve('/tmp', uuid.v4());
|
221 | var bundlePath = path.resolve(buildLocation, 'bundle.tar.gz');
|
222 |
|
223 |
|
224 |
|
225 | process.env.BUILD_LOCATION = buildLocation;
|
226 |
|
227 | var deployCheckWaitTime = this.config.deployCheckWaitTime;
|
228 | var appName = this.config.appName;
|
229 | var appPath = this.config.app;
|
230 | var buildOptions = this.config.buildOptions;
|
231 |
|
232 | console.log('Meteor app path : ' + this.config.app);
|
233 | console.log('Using buildOptions : ' + JSON.stringify(buildOptions));
|
234 | buildApp(appPath, buildLocation, buildOptions, function (err) {
|
235 | if (err) {
|
236 | process.exit(1);
|
237 | } else {
|
238 | var sessionsData = [];
|
239 | _.forEach(self.sessionsMap, function (sessionsInfo) {
|
240 | var taskListsBuilder = sessionsInfo.taskListsBuilder;
|
241 | _.forEach(sessionsInfo.sessions, function (session) {
|
242 | sessionsData.push({
|
243 | taskListsBuilder: taskListsBuilder,
|
244 | session: session
|
245 | });
|
246 | });
|
247 | });
|
248 |
|
249 | async.mapSeries(
|
250 | sessionsData,
|
251 | function (sessionData, callback) {
|
252 | var session = sessionData.session;
|
253 | var taskListsBuilder = sessionData.taskListsBuilder
|
254 | var env = _.extend({}, self.config.env, session._serverConfig.env);
|
255 | var taskList = taskListsBuilder.deploy(
|
256 | bundlePath, env, self.config);
|
257 | taskList.run(session, function (summaryMap) {
|
258 | callback(null, summaryMap);
|
259 | });
|
260 | },
|
261 | whenAfterDeployed(buildLocation)
|
262 | )
|
263 | }
|
264 | });
|
265 | }
|
266 |
|
267 | Actions.prototype.deployNextJS = function () {
|
268 | var self = this;
|
269 | var buildLocation = path.resolve('/tmp', uuid.v4());
|
270 | var bundlePath = path.resolve(buildLocation, 'bundle.tar.gz');
|
271 | try {
|
272 | fs.mkdirSync(buildLocation);
|
273 | } catch(err) {
|
274 | console.log(err);
|
275 | }
|
276 | process.env.BUILD_LOCATION = buildLocation;
|
277 |
|
278 | var deployCheckWaitTime = this.config.deployCheckWaitTime;
|
279 | var appName = this.config.appName;
|
280 | var appPath = this.config.app;
|
281 | var dirExclusions = this.config.dirExclusions && Object.prototype.toString.call(this.config.dirExclusions) === '[object Array]' ? NEXTJS_DIR_DEFAULT_EXCLUSIONS.concat(this.config.dirExclusions) : NEXTJS_DIR_DEFAULT_EXCLUSIONS;
|
282 |
|
283 | console.log('NextJS app path : ' + this.config.app);
|
284 | buildAppNextJS(appPath, buildLocation, dirExclusions, function (err) {
|
285 | if (err) {
|
286 | console.log(err);
|
287 | process.exit(1);
|
288 | } else {
|
289 | var sessionsData = [];
|
290 | _.forEach(self.sessionsMap, function (sessionsInfo) {
|
291 | var taskListsBuilder = sessionsInfo.taskListsBuilder;
|
292 | _.forEach(sessionsInfo.sessions, function (session) {
|
293 | sessionsData.push({
|
294 | taskListsBuilder: taskListsBuilder,
|
295 | session: session
|
296 | });
|
297 | });
|
298 | });
|
299 |
|
300 | async.mapSeries(
|
301 | sessionsData,
|
302 | function (sessionData, callback) {
|
303 | var session = sessionData.session;
|
304 | var taskListsBuilder = sessionData.taskListsBuilder
|
305 | var env = _.extend({}, self.config.env, session._serverConfig.env);
|
306 | var taskList = taskListsBuilder.deploy(
|
307 | bundlePath, env, self.config);
|
308 | taskList.run(session, function (summaryMap) {
|
309 | callback(null, summaryMap);
|
310 | });
|
311 | },
|
312 | whenAfterDeployed(buildLocation)
|
313 | )
|
314 | }
|
315 | });
|
316 | }
|
317 |
|
318 | Actions.prototype.deploy = function () {
|
319 | if(this.config.nextjs === true) {
|
320 | this.deployNextJS();
|
321 | } else {
|
322 | this.deployMeteor();
|
323 | }
|
324 | };
|
325 |
|
326 | Actions.prototype.reconfig = function () {
|
327 | var self = this;
|
328 | var sessionInfoList = [];
|
329 | for (var os in self.sessionsMap) {
|
330 | var sessionsInfo = self.sessionsMap[os];
|
331 | sessionsInfo.sessions.forEach(function (session) {
|
332 | var env = _.extend({}, self.config.env, session._serverConfig.env);
|
333 | var taskList = sessionsInfo.taskListsBuilder.reconfig(
|
334 | env, self.config);
|
335 | sessionInfoList.push({
|
336 | taskList: taskList,
|
337 | session: session
|
338 | });
|
339 | });
|
340 | }
|
341 |
|
342 | async.mapSeries(
|
343 | sessionInfoList,
|
344 | function (sessionsInfo, callback) {
|
345 | sessionsInfo.taskList.run(sessionsInfo.session, function (summaryMap) {
|
346 | callback(null, summaryMap);
|
347 | });
|
348 | },
|
349 | whenAfterCompleted
|
350 | );
|
351 | };
|
352 |
|
353 | Actions.prototype.restart = function () {
|
354 | this._executePararell("restart", [this.config]);
|
355 | };
|
356 |
|
357 | Actions.prototype.stop = function () {
|
358 | this._executePararell("stop", [this.config]);
|
359 | };
|
360 |
|
361 | Actions.prototype.start = function () {
|
362 | this._executePararell("start", [this.config]);
|
363 | };
|
364 |
|
365 | Actions.prototype.logs = function () {
|
366 | var self = this;
|
367 | var tailOptions = process.argv.slice(3).join(" ");
|
368 |
|
369 | var sessions = [];
|
370 |
|
371 | for (var os in self.sessionsMap) {
|
372 | var sessionsInfo = self.sessionsMap[os];
|
373 | sessionsInfo.sessions.forEach(function (session) {
|
374 | sessions.push(session);
|
375 | });
|
376 | }
|
377 |
|
378 | async.map(
|
379 | sessions,
|
380 | function (session, callback) {
|
381 | var hostPrefix = '[' + session._host + '] ';
|
382 | var options = {
|
383 | onStdout: function (data) {
|
384 | process.stdout.write(hostPrefix + data.toString());
|
385 | },
|
386 | onStderr: function (data) {
|
387 | process.stderr.write(hostPrefix + data.toString());
|
388 | }
|
389 | };
|
390 |
|
391 | var command = 'sudo docker logs ' + tailOptions + ' ' + self.config.appName;
|
392 | session.execute(command, options, callback);
|
393 | },
|
394 | whenAfterCompleted
|
395 | );
|
396 | };
|
397 |
|
398 | Actions.init = function () {
|
399 | var destMupJson = path.resolve('mup.json');
|
400 | var destSettingsJson = path.resolve('settings.json');
|
401 |
|
402 | if (fs.existsSync(destMupJson) || fs.existsSync(destSettingsJson)) {
|
403 | console.error('A Project Already Exists'.bold.red);
|
404 | process.exit(1);
|
405 | }
|
406 |
|
407 | var exampleMupJson = path.resolve(__dirname, '../example/mup.json');
|
408 | var exampleSettingsJson = path.resolve(__dirname, '../example/settings.json');
|
409 |
|
410 | copyFile(exampleMupJson, destMupJson);
|
411 | copyFile(exampleSettingsJson, destSettingsJson);
|
412 |
|
413 | console.log('Empty Project Initialized!'.bold.green);
|
414 |
|
415 | function copyFile(src, dest) {
|
416 | var content = fs.readFileSync(src, 'utf8');
|
417 | fs.writeFileSync(dest, content);
|
418 | }
|
419 | };
|
420 |
|
421 | function storeLastNChars(vars, field, limit, color) {
|
422 | return function (data) {
|
423 | vars[field] += data.toString();
|
424 | if (vars[field].length > 1000) {
|
425 | vars[field] = vars[field].substring(vars[field].length - 1000);
|
426 | }
|
427 | }
|
428 | }
|
429 |
|
430 | function whenAfterDeployed(buildLocation) {
|
431 | return function (error, summaryMaps) {
|
432 | rimraf.sync(buildLocation);
|
433 | whenAfterCompleted(error, summaryMaps);
|
434 | };
|
435 | }
|
436 |
|
437 | function whenAfterCompleted(error, summaryMaps) {
|
438 | var errorCode = error || haveSummaryMapsErrors(summaryMaps) ? 1 : 0;
|
439 | process.exit(errorCode);
|
440 | }
|
441 |
|
442 | function haveSummaryMapsErrors(summaryMaps) {
|
443 | return _.some(summaryMaps, hasSummaryMapErrors);
|
444 | }
|
445 |
|
446 | function hasSummaryMapErrors(summaryMap) {
|
447 | return _.some(summaryMap, function (summary) {
|
448 | return summary.error;
|
449 | })
|
450 | }
|