UNPKG

cd-cluster

Version:

A lightweight Continuous Deployment / Continuous Integration platform written in node.

552 lines (529 loc) 13.7 kB
var qs = require('qs'); var zlib = require('zlib'); var path = require('path'); var http = require('http'); var tar = require('tar-fs'); var fs = require('fs-extra'); var async = require('async'); var minimatch = require("minimatch"); var spawn = require('child_process').spawn; var utils = require('../libs/utils'); var errFactory = require('../libs/err_factory'); var userModule = require('../modules/user'); var historyModule = require('../modules/history'); var tasks = {}; var projects = utils.readConfig('projects') || []; exports.getProjects = function () { return projects; }; exports.getProject = function (name) { return projects.filter(function (project) { return project['name'] === name; })[0]; }; exports.addProject = function (project, next) { projects.push(project); utils.writeConfig('projects', projects, next); }; exports.updateProject = function (name, data, next) { var shouldCleanWorkspace = false; utils.forEach(projects, function (project, i) { if (project['name'] === name) { projects[i] = data; shouldCleanWorkspace = data['repo_type'] !== project['repo_type'] || data['repo_url'] !== project['repo_url'] || data['repo_branch'] !== project['repo_branch']; return false; } }); async.parallel([ function (next) { utils.writeConfig('projects', projects, next); }, function (next) { if (shouldCleanWorkspace) { exports.cleanWorkspace(name, next); } else { next(); } }, function (next) { if (shouldCleanWorkspace) { exports.abortProject(name, next); } else { next(); } } ], function (err) { next && next(err); }); }; exports.deleteProject = function (name, next) { utils.forEach(projects, function (project, i) { if (project['name'] === name) { projects.splice(i, 1); return false; } }); async.parallel([ function (next) { utils.writeConfig('projects', projects, next); }, function (next) { exports.cleanWorkspace(name, next); }, function (next) { historyModule.cleanHistories(name, next); } ], function (err) { next(err); }); }; exports.buildProject = function (name, operator, params, next) { var project = exports.getProject(name); if (!project) { next(); return; } var step; var history; var historyId; var ignores; var deployNodes; params = params || {}; async.waterfall([ function (next) { historyModule.addHistory(name, operator, historyModule.STATUS_UPDATING, next); }, function (data, next) { history = data; historyId = history['id']; historyModule.setHistorySize(name, +project['history_size'] || 1, next); }, function (next) { exports.ensureWorkspace(name, next); }, function (made, next) { var command; var repoType = project['repo_type']; var repoUrl = project['repo_url']; var repoBranch = project['repo_branch'] || 'master'; if (repoType === 'git') { if (made) { command = 'git clone --recurse --progress --depth 1 -b ' + repoBranch + ' ' + repoUrl + ' ./'; } else { command = '' + 'git pull\n' + 'git submodule init\n' + 'git submodule sync --recursive\n' + 'git submodule update --init --recursive --depth 1'; } } else if (repoType === 'svn') { if (made) { command = 'svn co ' + repoUrl + ' ./'; } else { command = 'svn up'; } } step = 'checkout'; runCommand(name, historyId, step, command, function (err) { if (err) { exports.cleanWorkspace(name); } next(err); }); }, function (next) { step = 'build'; history['status'] = historyModule.STATUS_BUILDING; historyModule.updateHistory(name, historyId, history, next); }, function (next) { runCommand(name, historyId, step, project['build_scripts'], next); }, function (next) { step = 'test'; history['status'] = historyModule.STATUS_TESTING; historyModule.updateHistory(name, historyId, history, next); }, function (next) { runCommand(name, historyId, step, project['test_scripts'], next); }, function (next) { step = 'pack'; history['status'] = historyModule.STATUS_PACKING; historyModule.updateHistory(name, historyId, history, next); }, function (next) { ignores = params['ignores']; if (!ignores && project['ignores']) { ignores = project['ignores'].split('\n'); } var ignoreStr = ignores.join('\n ') || 'Empty'; historyModule.writeOutput(name, historyId, step, 'Packing files...\n\nIgnores:\n ' + ignoreStr + '\n', next); }, function (next) { exports.packProject(name, historyId, ignores, next); }, function (next) { history['build_url'] = historyModule.getBuildUrl(name, historyId); historyModule.updateHistory(name, historyId, history, next); }, function (next) { historyModule.writeOutput(name, historyId, step, '\nDone.', next); }, function (next) { step = 'deploy'; history['status'] = historyModule.STATUS_DEPLOYING; historyModule.updateHistory(name, historyId, history, next); }, function (next) { deployNodes = params['deploy_nodes'] || project['deploy_nodes']; var nodeStr = deployNodes.map(function (node) { return ' ' + node['host'] + ':' + node['port']; }).join('\n') || 'Empty'; historyModule.writeOutput(name, historyId, step, 'Nodes:\n' + nodeStr + '\n', next); }, function (next) { exports.deployProject(name, historyId, deployNodes, next); }, function (result, next) { historyModule.writeOutput(name, historyId, step, '\n' + result, next); } ], function (err) { var startTime = history['start_time']; history['duration'] = Date.now() - startTime; if (err) { history['status'] = historyModule.STATUS_FAILED; historyModule.writeOutput(name, historyId, step, (err.desc || err.message) + '\n' + '\u001b[31mFailed.\u001b[39m'); } else { history['status'] = historyModule.STATUS_SUCCESS; } historyModule.updateHistory(name, historyId, history, next); }); }; exports.abortProject = function (name, next) { var task = tasks[name]; var project = exports.getProject(name); if (task && project) { var historyId = task['id']; var process = task['process']; var archive = task['archive']; var request = task['request']; var history = historyModule.getHistory(name, historyId); function updateHistory() { history['status'] = historyModule.STATUS_ABORTED; historyModule.updateHistory(name, historyId, history, next); } if (request) { request.abort(); updateHistory(); } else if (archive) { archive.destroy(); updateHistory(); } else if (process) { if (process.killed || process.exitCode !== null) { updateHistory(); } else { process.kill(); process.on('exit', function () { updateHistory(); }); } } else { updateHistory(); } delete tasks[name]; } else { next(); } }; exports.packProject = function (name, historyId, ignores, next) { var project = exports.getProject(name); if (!project) { next(); return; } var task = tasks[name]; var workspace = exports.getWorkspace(name); var buildPath = historyModule.getBuildPath(name, historyId); async.waterfall([ function (next) { fs.ensureDir(path.dirname(buildPath), next); }, function (made, next) { if (ignores) { ignores = ignores.map(function (item) { if (item.slice(-1) === '/') { item += '**'; } return item; }); } var archive = tar.pack(workspace, { ignore: function (filename) { for (var i = 0, l = ignores.length; i < l; ++i) { var relative = path.relative(workspace, filename); try { if (fs.statSync(filename).isDirectory()) { relative += '/'; } } catch (e) { } if (minimatch(relative, ignores[i])) { return true; } } } }); archive.pipe(zlib.Gzip()).pipe(fs.createWriteStream(buildPath)).on('finish', next); if (!task) { task = tasks[name] = { id: historyId, archive: archive }; } } ], function (err) { if (task && task['archive']) { delete tasks[name]; } next(err); }); }; exports.deployProject = function (name, historyId, nodes, next) { var project = exports.getProject(name); if (!project) { next(); return; } var task = tasks[name]; var buildPath = historyModule.getBuildPath(name, historyId); var stream = fs.createReadStream(buildPath); if (!task) { task = tasks[name] = { id: historyId, requests: [] } } nodes = nodes || project['deploy_nodes']; async.map(nodes, function (node, next) { var data = { name: name, cwd: node['cwd'], host: node['host'], token: node['token'], history_id: historyId, env_vars: project['env_vars'], pre_deploy_scripts: project['pre_deploy_scripts'], post_deploy_scripts: project['post_deploy_scripts'] }; var finished = false; var query = qs.stringify(data); var host = node['host']; var port = node['port']; var req = http.request({ host: host, port: port, method: 'post', path: '/deploy?' + query }, function (res) { utils.receiveJSON(res, function (err, json) { if (!finished) { finished = true; next(err, json); } }); }); req.on('error', function (err) { if (!finished) { finished = true; next(err); } }); stream.pipe(req); if (task) { task['requests'].push(req); } }, function (err, results) { if (task && task['requests']) { delete tasks[name]; } if (err) { next(err); } else { resolveNodeResults(results, nodes, next); } }); }; exports.executeScript = function (name, scriptId, next) { var project = exports.getProject(name); if (!project) { next(); return; } var nodes = project['deploy_nodes']; var operationScript = project['operation_scripts'] || []; var script = operationScript[scriptId]; async.map(nodes, function (node, next) { var data = { name: name, cwd: node['cwd'], host: node['host'], token: node['token'], script_id: scriptId, command: script['command'], env_vars: project['env_vars'] }; var finished = false; var query = qs.stringify(data); var host = node['host']; var port = node['port']; var req = http.request({ host: host, port: port, method: 'post', path: '/execute?' + query }, function (res) { utils.receiveJSON(res, function (err, json) { if (!finished) { finished = true; next(err, json); } }); }); req.on('error', function (err) { if (!finished) { finished = true; next(err); } }); req.end(); }, function (err, results) { if (err) { next(err); } else { resolveNodeResults(results, nodes, next); } }); }; exports.ensureWorkspace = function (name, next) { fs.ensureDir(exports.getWorkspace(name), next); }; exports.cleanWorkspace = function (name, next) { fs.remove(exports.getWorkspace(name), next); }; exports.getWorkspace = function (name) { var configDir = utils.getConfigDir(); return configDir + '/workspace/' + name; }; exports.getBuildEnv = function (name, historyId) { var project = exports.getProject(name) || {}; return utils.extend({}, process.env, { PROJECT_NAME: name, BUILD_ID: historyId }, project['env_vars']); }; exports.checkPermission = function (user, project, next) { var username = user['username']; var managers = project['managers'] || []; user = userModule.getUser(username); if (user && user['is_admin'] && user['enabled'] || managers.indexOf(username) >= 0) { next(); } else { next(errFactory.unauthorized()); } }; exports.removeManager = function (manager, next) { utils.forEach(projects, function (project) { var managers = project['managers']; var index = managers.indexOf(manager); if (index >= 0) { managers.splice(index, 1); return false; } }); utils.writeConfig('projects', projects, next); }; function resolveNodeResults(results, nodes, next) { var err; var output = ''; var failed = false; if (results) { output = results.map(function (res, i) { var msg = ''; var node = nodes[i]; var host = node['host']; if (res && res['data']) { msg += res['data']; } if (res && res['error']) { failed = true; msg += '\n\n' + res['error_desc'] || res['error']; } return '\u001b[1m' + host + ':\u001b[22m\n' + msg; }).join('\n\n'); } if (failed) { err = errFactory.runtimeError(output); } next(err, output); } function runCommand(name, historyId, step, command, next) { if (!command) { next(); return; } var configDir = utils.getConfigDir(); var env = exports.getBuildEnv(name, historyId); var commandFile = configDir + '/tmp/' + name + '-' + historyId + '-' + step; async.waterfall([ function (next) { historyModule.writeOutput(name, historyId, step, command + '\n', next); }, function (next) { fs.outputFile(commandFile, command, next); }, function (next) { var finished = false; var workspace = exports.getWorkspace(name); var p = spawn('sh', [commandFile], { cwd: workspace, env: env }); p.stdout.on('data', function (data) { historyModule.writeOutput(name, historyId, step, data); }); p.stderr.on('data', function (data) { historyModule.writeOutput(name, historyId, step, data); }); p.on('close', function (code) { if (!finished) { finished = true; if (code) { next(errFactory.runtimeError('Process exited with code ' + code)); } else { next(); } } }); p.on('error', function (err) { if (!finished) { finished = true; next(err); } }); tasks[name] = { id: historyId, process: p }; } ], function (err) { fs.remove(commandFile); if (tasks[name]) { delete tasks[name]; next(err); } }); }