UNPKG

nci

Version:

Flexible, open source continuous integration server written in node.js

730 lines (639 loc) 16.4 kB
'use strict'; var Steppy = require('twostep').Steppy, fs = require('fs'), path = require('path'), _ = require('underscore'), utils = require('./utils'), SpawnCommand = require('./command/spawn').Command, validateParams = require('./validateParams'), EventEmitter = require('events').EventEmitter, inherits = require('util').inherits, junk = require('junk'); /** * Projects collection contains all currently loaded projects and provides * operations for manipulating with them. * All projects stored on disk in `baseDir` and `archiveDir` and loaded to * memory so they can be received (by `get`, `getAll` and other methods) in a * sync way. * Note that id for the particular project is a `name` of that project. */ function ProjectsCollection(params) { this.db = params.db; this.reader = params.reader; this.baseDir = params.baseDir; this.archiveDir = params.archiveDir; this.configs = []; this.loadingProjectsHash = {}; } exports.ProjectsCollection = ProjectsCollection; inherits(ProjectsCollection, EventEmitter); /** * Validate and return given config. * * @param {Object} config * @param {Function} callback(err,config) */ ProjectsCollection.prototype.validateConfig = function(config, callback) { Steppy( function() { validateParams(config, { type: 'object', properties: { name: { type: 'string', pattern: /^(\w|-)+$/ }, scm: { type: 'object', required: true, properties: { type: {type: 'string', required: true}, repository: {type: 'string', required: true}, rev: {type: 'string', required: true} } }, steps: { type: 'array', required: true, items: { type: 'object', properties: { cmd: {type: 'string', required: true}, name: {type: 'string'}, type: {'enum': ['shell']}, shell: {type: 'string'}, shellCmdArg: {type: 'string'}, shellExtraArgs: { type: 'array', items: {type: 'string'} } } } } }, additionalProperties: true }); this.pass(null); }, function(err) { if (err) { err.message = ( 'Error during validation of project "' + config.name + '": ' + err.message ); } callback(err, config); } ); }; ProjectsCollection.prototype._getProjectPath = function(params) { return path.join( params.archived ? this.archiveDir : this.baseDir, params.name ); }; ProjectsCollection.prototype._projectPathExists = function(params, callback) { var self = this; Steppy( function() { var stepCallback = this.slot(); fs.exists(self._getProjectPath(params), function(exists) { stepCallback(null, exists); }); }, callback ); }; ProjectsCollection.prototype._loadConfig = function(dir, callback) { var self = this; Steppy( function() { self.reader.load(dir, 'config', this.slot()); }, function(err, config) { // convert steps object to array if (!_(config.steps).isArray() && _(config.steps).isObject()) { config.steps = _(config.steps).map(function(val, name) { var step; if (_(val).isObject()) { step = val; } else { step = {cmd: val}; } step.name = name; return step; }); } // apply defaults _(config.steps).each(function(step) { if (!step.type) step.type = 'shell'; if (!step.name && step.cmd) step.name = utils.prune(step.cmd, 40); }); this.pass(config); }, callback ); }; /** * Load project to collection. * `projectLoaded` event with loaded config as argument will be emitted after * load. * - `params.name` - name of the project to load * - `params.archived` - if true then try to load archived project * * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.load = function(params, callback) { // fallback for old signature when only name were passed if (!_(params).isObject()) { params = {name: params}; } callback = callback || _.noop; var self = this, name = params.name, archived = params.archived, dir; Steppy( function() { if (!name) { throw new Error('Project name is required'); } dir = self._getProjectPath(params); // if project already loaded or loading just quit if (self.get(name) || self.loadingProjectsHash[name]) { return callback(); } self.loadingProjectsHash[name] = 1; self._loadConfig(dir, this.slot()); }, function(err, config) { config.name = name; config.dir = dir; config.archived = Boolean(archived); self.validateConfig(config, this.slot()); }, function(err, config) { self.configs.push(config); self.emit('projectLoaded', config); this.pass(null); }, function(err) { delete self.loadingProjectsHash[name]; callback(err); } ); }; /** * Load all projects (from `this.baseDir`). * Calls `load` for every project in a base dir. * * @param {Function} [callback(err)] */ ProjectsCollection.prototype.loadAll = function(callback) { callback = callback || _.noop; var self = this; Steppy( function() { fs.readdir(self.baseDir, this.slot()); fs.readdir(self.archiveDir, this.slot()); }, function(err, dirs, archivedDirs) { dirs = _(dirs).filter(junk.not); var loadGroup = this.makeGroup(); _(dirs).each(function(dir) { self.load({name: dir}, loadGroup.slot()); }); archivedDirs = _(archivedDirs).filter(junk.not); var archiveLoadGroup = this.makeGroup(); _(archivedDirs).each(function(dir) { self.load({name: dir, archived: true}, archiveLoadGroup.slot()); }); }, callback ); }; /** * Unload project from collection * `projectUnloaded` event with unloaded config as argument will be emitted * after unload. * - `params.name` - name of the project to unload * * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.unload = function(params, callback) { // fallback for old signature when only name were passed if (!_(params).isObject()) { params = {name: params}; } callback = callback || _.noop; var self = this, name = params.name; Steppy( function() { var index = _(self.configs).findIndex(function(config) { return config.name === name; }); if (index === -1) { throw new Error('Can`t unload not loaded project: "' + name + '"'); } var unloadedConfig = self.configs.splice(index, 1)[0]; self.emit('projectUnloaded', unloadedConfig); this.pass(null); }, callback ); }; ProjectsCollection.prototype._archiveUnarchive = function(params, callback) { var self = this, name = params.name, action = params.action, project; Steppy( function() { if (!name) { throw new Error('Project name is required'); } project = self.get(name); if (!project) { throw new Error('Can`t find project "' + name + '" for ' + action); } if (action === 'archive') { if (project.archived) { throw new Error('Project "' + name + '" already archived'); } } else if (action === 'unarchive') { if (!project.archived) { throw new Error('Project "' + name + '" is not archived'); } } else { throw new Error('Unknown action: ' + action); } self.unload({name: name}, this.slot()); }, function() { var newPath = self._getProjectPath({ name: name, archived: (action === 'archive') }); fs.rename(project.dir, newPath, this.slot()); }, function() { self.load({ name: name, archived: (action === 'archive') }, this.slot()); }, callback ); }; /** * Archive project. * - `params.name` - name of the project to archive * * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.archive = function(params, callback) { callback = callback || _.noop; var self = this, name = params.name; Steppy( function() { self._archiveUnarchive({ name: name, action: 'archive' }, this.slot()); }, callback ); }; /** * Unarchive project. * - `params.name` - name of the project to unarchive * * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.unarchive = function(params, callback) { callback = callback || _.noop; var self = this, name = params.name; Steppy( function() { self._archiveUnarchive({ name: name, action: 'unarchive' }, this.slot()); }, callback ); }; /** * Reload project. * - `params.name` - name of the project to reload * * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.reload = function(params, callback) { // fallback for old signature when only name were passed if (!_(params).isObject()) { params = {name: params}; } callback = callback || _.noop; var self = this, name = params.name, project; Steppy( function() { project = self.get(name); if (project) { self.unload({name: name}, this.slot()); } else { this.pass(null); } }, function(err) { var loadParams = {name: name}; if (_(project).has('archived')) { loadParams.archived = project.archived; } self.load(loadParams, this.slot()); }, callback ); }; /** * Get project config by name. * Returns config object or undefined if project is not found. * * @param {String} name */ ProjectsCollection.prototype.get = function(name) { return _(this.configs).findWhere({name: name}); }; /** * Get configs for all currently loaded projects. * Returns array of config objects. */ ProjectsCollection.prototype.getAll = function() { return this.configs; }; /** * Get project configs which match to predicate. * Returns array of config objects or empty array if there is no matched * project. * * @param {Function} predicate */ ProjectsCollection.prototype.filter = function(predicate) { return _(this.configs).filter(predicate); }; /** * Remove project. * Calls `unload`, removes project from disk and db. * - `params.name` - name of the project to remove * * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.remove = function(params, callback) { // fallback for old signature when only name were passed if (!_(params).isObject()) { params = {name: params}; } callback = callback || _.noop; var self = this, name = params.name; Steppy( function() { if (!name) { throw new Error('Project name is required'); } var project = self.get(name); if (!project) { throw new Error('Can`t find project "' + name + '" for removing'); } self.db.builds.find({ start: {projectName: name, descCreateDate: ''} }, this.slot()); new SpawnCommand().run({cmd: 'rm', args: [ '-Rf', project.dir ]}, this.slot()); self.unload({name: name}, this.slot()); }, function(err, builds) { if (builds.length) { self.db.builds.del(builds, this.slot()); var logLinesRemoveGroup = this.makeGroup(); _(builds).each(function(build) { self.db.logLines.remove({ start: {buildId: build.id} }, logLinesRemoveGroup.slot()); }); } else { this.pass(null, null); } }, callback ); }; /** * Rename project. * Renames project on disk and db, also changes name for loaded project. * - `params.name` - name of the project to rename * - `params.newName` - new name of the project * * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.rename = function(params, callback) { // fallback for old signature when name and newName were passes as // separate params if (!_(params).isObject()) { params = { name: arguments[0], newName: arguments[1] }; callback = arguments[2]; } callback = callback || _.noop; var self = this, name = params.name, newName = params.newName, project; Steppy( function() { if (!name) { throw new Error('Project name is required'); } if (!newName) { throw new Error('Project new name is required'); } project = self.get(name); if (!project) { throw new Error('Can`t find project "' + name + '" for rename'); } self.unload({name: name}, this.slot()); }, function() { var newPath = self._getProjectPath({ name: newName, archived: project.archived }); fs.rename(project.dir, newPath, this.slot()); self.db.builds.multiUpdate( {start: {projectName: name, descCreateDate: ''}}, function(build) { build.project.name = newName; return build; }, this.slot() ); }, function() { self.load({name: newName, archived: project.archived}, this.slot()); }, callback ); }; /** * Create project. * - `params.name` - name of the project * - `params.config` - project configuratjion object * - `params.configFile` - project cconfig file object with `name` and * `content` fields (it's alternative for `config` option when need to set file * in specific format) * - `params.load` - if true then project will be loaded * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.create = function(params, callback) { callback = callback || _.noop; var self = this, name = params.name, dir; Steppy( function() { if (!name) { throw new Error('Project name is required'); } self._projectPathExists({name: name}, this.slot()); self._projectPathExists({name: name, archived: true}, this.slot()); }, function(err, projectPathExists, archivedProjectPathExists) { if (projectPathExists || archivedProjectPathExists) { throw new Error('Project "' + name + '" already exists'); } dir = self._getProjectPath({name: name}); fs.mkdir(dir, this.slot()); }, function(err, setConfigParams) { self.setConfig( _({projectDir: dir}).extend( _(params).pick('config', 'configFile', 'load') ), this.slot() ); }, function(err) { if (err) { if (dir) { // try to remove project dir to prevent dir without config fs.rmdir(dir, function(err) { if (err) { console.error('Error while removing project dir: ' + dir); } }); } } callback(err); } ); }; /** * Set config file for the project. * - `params.projectName` - name of the project * - `params.config` - project configuratjion object * - `params.configFile` - project cconfig file object with `name` and * `content` fields (it's alternative for `config` option when need to set file * in specific format) * - `params.load` - if true then project will be loaded * @param {Object} params * @param {Function} [callback(err)] */ ProjectsCollection.prototype.setConfig = function(params, callback) { callback = callback || _.noop; var self = this, projectName = params.projectName, projectDir = params.projectDir, config = params.config, load = params.load; Steppy( function() { if (projectName) { var project = self.get(projectName); if (!project) { throw new Error('Project "' + projectName + '" doesn`t exist'); } this.pass(project.dir); } else if (projectDir) { var stepCallback = this.slot(); fs.exists(projectDir, function(exists) { if (exists) { stepCallback( null, projectDir ); } else { stepCallback( new Error('Project dir "' + projectDir + '" doesn`t exist'), null ); } }); } else { throw new Error('`projectName` or `projectDir` option is required'); } }, function(err, projectPath) { this.pass(projectPath); if (config) { self.validateConfig(config, this.slot()); } else { this.pass(null); } }, function(err, projectPath) { var configFile; if (config) { configFile = { name: 'config.json', content: JSON.stringify(config, null, 4) }; } else if (params.configFile) { configFile = params.configFile; } else { throw new Error('`config` or `configFile` option is required'); } // TODO: remove all configs from projects dir fs.writeFile( path.join(projectPath, configFile.name), configFile.content, {encoding: 'utf-8'}, this.slot() ); }, function(err) { var name = projectName ? projectName : path.basename(projectDir); if (load) { self.reload({name: name}, this.slot()); } else { this.pass(null); } }, callback ); };