Containerizer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. var spawn = require('child_process').spawn;
  2. var exec = require('child_process').exec;
  3. var chalk = require('chalk');
  4. var util = require('util');
  5. var fmt = require('../tools/fmt.js');
  6. var fs = require('fs');
  7. var path = require('path');
  8. var cst = require('../../constants.js');
  9. var Promise = require('../tools/promise.min.js');
  10. function pspawn(cmd) {
  11. return new Promise(function(resolve, reject) {
  12. var p_cmd = cmd.split(' ');
  13. var install_instance = spawn(p_cmd[0], p_cmd.splice(1, cmd.length), {
  14. stdio : 'inherit',
  15. env : process.env,
  16. shell : true
  17. });
  18. install_instance.on('close', function(code) {
  19. if (code != 0) {
  20. console.log(chalk.bold.red('Command failed'));
  21. return reject(new Error('Bad cmd return'));
  22. }
  23. return resolve();
  24. });
  25. install_instance.on('error', function (err) {
  26. return reject(err);
  27. });
  28. });
  29. }
  30. function checkDockerSetup() {
  31. return new Promise(function(resolve, reject) {
  32. exec("docker version -f '{{.Client.Version}}'", function(err, stdout, stderr) {
  33. if (err) {
  34. console.error(chalk.red.bold('[Docker access] Error while trying to use docker command'));
  35. if (err.message && err.message.indexOf('Cannot connect to the Docker') > -1) {
  36. console.log();
  37. console.log(chalk.blue.bold('[Solution] Setup Docker to be able to be used without sudo rights:'));
  38. console.log(chalk.bold('$ sudo groupadd docker'));
  39. console.log(chalk.bold('$ sudo usermod -aG docker $USER'));
  40. console.log(chalk.bold('Then LOGOUT and LOGIN your Linux session'));
  41. console.log('Read more: http://bit.ly/29JGdCE');
  42. }
  43. return reject(err);
  44. }
  45. return resolve();
  46. });
  47. });
  48. }
  49. /**
  50. * Switch Dockerfile mode
  51. * check test/programmatic/containerizer.mocha.js
  52. */
  53. function parseAndSwitch(file_content, main_file, opts) {
  54. var lines = file_content.split('\n');
  55. var mode = opts.mode;
  56. lines[0] = 'FROM keymetrics/pm2:' + opts.node_version;
  57. for (var i = 0; i < lines.length; i++) {
  58. var line = lines[i];
  59. if (['## DISTRIBUTION MODE', '## DEVELOPMENT MODE'].indexOf(line) > -1 ||
  60. i == lines.length - 1) {
  61. lines.splice(i, lines.length);
  62. lines[i] = '## ' + mode.toUpperCase() + ' MODE';
  63. lines[i + 1] = 'ENV NODE_ENV=' + (mode == 'distribution' ? 'production' : mode);
  64. if (mode == 'distribution') {
  65. lines[i + 2] = 'COPY . /var/app';
  66. lines[i + 3] = 'CMD ["pm2-docker", "' + main_file + '", "--env", "production"]';
  67. }
  68. if (mode == 'development') {
  69. lines[i + 2] = 'CMD ["pm2-dev", "' + main_file + '", "--env", "development"]';
  70. }
  71. break;
  72. }
  73. };
  74. lines = lines.join('\n');
  75. return lines;
  76. };
  77. /**
  78. * Replace ENV, COPY and CMD depending on the mode
  79. * @param {String} docker_filepath Dockerfile absolute path
  80. * @param {String} main_file Main file to start in container
  81. * @param {String} mode Mode to switch the Dockerfile
  82. */
  83. function switchDockerFile(docker_filepath, main_file, opts) {
  84. return new Promise(function(resolve, reject) {
  85. var data = fs.readFileSync(docker_filepath, 'utf8').toString();
  86. if (['distribution', 'development'].indexOf(opts.mode) == -1)
  87. return reject(new Error('Unknown mode'));
  88. var lines = parseAndSwitch(data, main_file, opts)
  89. fs.writeFile(docker_filepath, lines, function(err) {
  90. if (err) return reject(err);
  91. resolve({
  92. Dockerfile_path : docker_filepath,
  93. Dockerfile : lines,
  94. CMD : ''
  95. });
  96. })
  97. });
  98. }
  99. /**
  100. * Generate sample Dockerfile (lib/templates/Dockerfiles)
  101. * @param {String} docker_filepath Dockerfile absolute path
  102. * @param {String} main_file Main file to start in container
  103. * @param {String} mode Mode to switch the Dockerfile
  104. */
  105. function generateDockerfile(docker_filepath, main_file, opts) {
  106. return new Promise(function(resolve, reject) {
  107. var tpl_file = path.join(cst.TEMPLATE_FOLDER, cst.DOCKERFILE_NODEJS);
  108. var template = fs.readFileSync(tpl_file, {encoding: 'utf8'});
  109. var CMD;
  110. template = parseAndSwitch(template, main_file, opts);
  111. fs.writeFile(docker_filepath, template, function(err) {
  112. if (err) return reject(err);
  113. resolve({
  114. Dockerfile_path : docker_filepath,
  115. Dockerfile : template,
  116. CMD : CMD
  117. });
  118. });
  119. });
  120. }
  121. function handleExit(CLI, opts, mode) {
  122. process.on('SIGINT', function() {
  123. CLI.disconnect();
  124. if (mode != 'distribution')
  125. return false;
  126. exec('docker ps -lq', function(err, stdout, stderr) {
  127. if (err) {
  128. console.error(err);
  129. }
  130. require('vizion').analyze({folder : process.cwd()}, function recur_path(err, meta){
  131. if (!err && meta.revision) {
  132. var commit_id = util.format('#%s(%s) %s',
  133. meta.branch,
  134. meta.revision.slice(0, 5),
  135. meta.comment);
  136. console.log(chalk.bold.magenta('$ docker commit -m "%s" %s %s'),
  137. commit_id,
  138. stdout.replace('\n', ''),
  139. opts.imageName);
  140. }
  141. else
  142. console.log(chalk.bold.magenta('$ docker commit %s %s'), stdout.replace('\n', ''), opts.imageName);
  143. console.log(chalk.bold.magenta('$ docker push %s'), opts.imageName);
  144. });
  145. });
  146. });
  147. }
  148. module.exports = function(CLI) {
  149. CLI.prototype.generateDockerfile = function(script, opts) {
  150. var docker_filepath = path.join(process.cwd(), 'Dockerfile');
  151. var that = this;
  152. fs.stat(docker_filepath, function(err, stat) {
  153. if (err || opts.force == true) {
  154. generateDockerfile(docker_filepath, script, {
  155. mode : 'development'
  156. })
  157. .then(function() {
  158. console.log(chalk.bold('New Dockerfile generated in current folder'));
  159. console.log(chalk.bold('You can now run\n$ pm2 docker:dev <file|config>'));
  160. return that.exitCli(cst.SUCCESS_EXIT);
  161. });
  162. return false;
  163. }
  164. console.log(chalk.red.bold('Dockerfile already exists in this folder, use --force if you want to replace it'));
  165. that.exitCli(cst.ERROR_EXIT);
  166. });
  167. };
  168. CLI.prototype.dockerMode = function(script, opts, mode) {
  169. var promptly = require('promptly');
  170. var self = this;
  171. handleExit(self, opts, mode);
  172. if (mode == 'distribution' && !opts.imageName) {
  173. console.error(chalk.bold.red('--image-name [name] option is missing'));
  174. return self.exitCli(cst.ERROR_EXIT);
  175. }
  176. var template;
  177. var app_path, main_script;
  178. var image_name;
  179. var node_version = opts.nodeVersion ? opts.nodeVersion.split('.')[0] : 'latest';
  180. image_name = opts.imageName || require('crypto').randomBytes(6).toString('hex');
  181. if (script.indexOf('/') > -1) {
  182. app_path = path.join(process.cwd(), path.dirname(script));
  183. main_script = path.basename(script);
  184. }
  185. else {
  186. app_path = process.cwd();
  187. main_script = script;
  188. }
  189. checkDockerSetup()
  190. .then(function() {
  191. /////////////////////////
  192. // Generate Dockerfile //
  193. /////////////////////////
  194. return new Promise(function(resolve, reject) {
  195. var docker_filepath = path.join(process.cwd(), 'Dockerfile');
  196. fs.stat(docker_filepath, function(err, stat) {
  197. if (err) {
  198. // Dockerfile does not exist, generate one
  199. // console.log(chalk.blue.bold('Generating new Dockerfile'));
  200. if (opts.force == true) {
  201. return resolve(generateDockerfile(docker_filepath, main_script, {
  202. node_version : node_version,
  203. mode : mode
  204. }));
  205. }
  206. if (opts.dockerdaemon)
  207. return resolve(generateDockerfile(docker_filepath, main_script, {
  208. node_version : node_version,
  209. mode : mode
  210. }));
  211. promptly.prompt('No Dockerfile in current directory, ok to generate a new one? (y/n)', function(err, value) {
  212. if (value == 'y')
  213. return resolve(generateDockerfile(docker_filepath, main_script, {
  214. node_version : node_version,
  215. mode : mode
  216. }));
  217. else
  218. return self.exitCli(cst.SUCCESS_EXIT);
  219. });
  220. return false;
  221. }
  222. return resolve(switchDockerFile(docker_filepath, main_script, {
  223. node_version : node_version,
  224. mode : mode
  225. }));
  226. });
  227. });
  228. })
  229. .then(function(_template) {
  230. template = _template;
  231. return Promise.resolve();
  232. })
  233. .then(function() {
  234. //////////////////
  235. // Docker build //
  236. //////////////////
  237. var docker_build = util.format('docker build -t %s -f %s',
  238. image_name,
  239. template.Dockerfile_path);
  240. if (opts.fresh == true)
  241. docker_build += ' --no-cache';
  242. docker_build += ' .';
  243. console.log();
  244. fmt.sep();
  245. fmt.title('Building Boot System');
  246. fmt.field('Type', chalk.cyan.bold('Docker'));
  247. fmt.field('Mode', mode);
  248. fmt.field('Image name', image_name);
  249. fmt.field('Docker build command', docker_build);
  250. fmt.field('Dockerfile path', template.Dockerfile_path);
  251. fmt.sep();
  252. return pspawn(docker_build);
  253. })
  254. .then(function() {
  255. ////////////////
  256. // Docker run //
  257. ////////////////
  258. var docker_run = 'docker run --net host';
  259. if (opts.dockerdaemon == true)
  260. docker_run += ' -d';
  261. if (mode != 'distribution')
  262. docker_run += util.format(' -v %s:/var/app -v /var/app/node_modules', app_path);
  263. docker_run += ' ' + image_name;
  264. var dockerfile_parsed = template.Dockerfile.split('\n');
  265. var base_image = dockerfile_parsed[0];
  266. var run_cmd = dockerfile_parsed[dockerfile_parsed.length - 1];
  267. console.log();
  268. fmt.sep();
  269. fmt.title('Booting');
  270. fmt.field('Type', chalk.cyan.bold('Docker'));
  271. fmt.field('Mode', mode);
  272. fmt.field('Base Image', base_image);
  273. fmt.field('Image Name', image_name);
  274. fmt.field('Docker Command', docker_run);
  275. fmt.field('RUN Command', run_cmd);
  276. fmt.field('CWD', app_path);
  277. fmt.sep();
  278. return pspawn(docker_run);
  279. })
  280. .then(function() {
  281. console.log(chalk.blue.bold('>>> Leaving Docker instance uuid=%s'), image_name);
  282. self.disconnect();
  283. return Promise.resolve();
  284. })
  285. .catch(function(err) {
  286. console.log();
  287. console.log(chalk.grey('Raw error=', err.message));
  288. self.disconnect();
  289. });
  290. };
  291. };
  292. module.exports.generateDockerfile = generateDockerfile;
  293. module.exports.parseAndSwitch = parseAndSwitch;
  294. module.exports.switchDockerFile = switchDockerFile;