horticulturalist
Advanced tools
Comparing version 0.12.7 to 0.13.0
{ | ||
"name": "horticulturalist", | ||
"version": "0.12.7", | ||
"version": "0.13.0", | ||
"description": "A fancy gardener", | ||
@@ -15,4 +15,5 @@ "repository": "https://github.com/medic/horticulturalist", | ||
"start": "node src/index.js --dev", | ||
"test": "npm run unit-tests && npm run int-tests", | ||
"unit-tests": "jshint src/ tests/ && TESTING=1 mocha ./tests/unit/**.js", | ||
"eslint": "eslint src/**/* tests/**/*", | ||
"test": "npm run eslint && npm run unit-tests && npm run int-tests", | ||
"unit-tests": "TESTING=1 mocha ./tests/unit/**.js", | ||
"int-tests": "mocha --full-trace -s 10000 -t 65536 ./tests/int/tests/**.js" | ||
@@ -36,7 +37,7 @@ }, | ||
"chai-as-promised": "^7.1.1", | ||
"jshint": "^2.9.4", | ||
"eslint": "^5.12.1", | ||
"medic-builds-repo": "^0.3.0", | ||
"mocha": "^5.2.0", | ||
"sinon": "4.0.1" | ||
"sinon": "^7.2.5" | ||
} | ||
} |
Horticulturalist | ||
================ | ||
Deploys and manages [Medic](github.com/medic/medic-webapp). | ||
Deploys and manages [Medic](https://github.com/medic/medic). | ||
For more detailed documentation on how to start Medic using Horticulturalist, [see this guide](https://github.com/medic/medic-webapp#deploy-locally-using-horticulturalist-beta). | ||
For more detailed documentation on how to start Medic using Horticulturalist, [see this guide](https://github.com/medic/medic#deploy-locally-using-horticulturalist-beta). | ||
@@ -64,6 +64,2 @@ # Usage | ||
# Development | ||
Most development is managed in [the horticulturalist tag in medic/medic-webapp](https://github.com/medic/medic-webapp/issues?q=is%3Aopen+is%3Aissue+label%3Ahorticulturalist). | ||
## Releasing | ||
@@ -70,0 +66,0 @@ |
@@ -8,3 +8,4 @@ const request = require('request-promise-native'), | ||
const { info } = require('./log'); | ||
const { info, error } = require('./log'), | ||
help = require('./help'); | ||
@@ -24,3 +25,11 @@ const DEFAULT_BUILDS_URL = 'https://staging.dev.medicmobile.org/_couch/builds'; | ||
} else { | ||
const COUCH_URL = new URL(process.env.COUCH_URL); | ||
let COUCH_URL; | ||
try { | ||
COUCH_URL = new URL(process.env.COUCH_URL); | ||
} catch (err) { | ||
help.outputHelp(); | ||
error('You must define the COUCH_URL environment variable, pointing to the DB you wish to deploy into'); | ||
process.exit(-1); | ||
} | ||
COUCH_URL.pathname = '/'; | ||
@@ -42,3 +51,5 @@ | ||
const DEPLOY_URL = process.env.COUCH_URL; | ||
if(!DEPLOY_URL) throw new Error('COUCH_URL env var not set.'); | ||
if (!DEPLOY_URL) { | ||
throw new Error('COUCH_URL env var not set.'); | ||
} | ||
@@ -45,0 +56,0 @@ module.exports = { |
const fs = require('fs'), | ||
path = require('path'); | ||
const {info} = require('./log'); | ||
const pluckOptionsFromReadme = () => { | ||
const readmePath = path.join(__dirname, '..', 'README.md'); | ||
let readmeString = fs.readFileSync(readmePath, 'utf8'); | ||
let readmeString = '\n' + fs.readFileSync(readmePath, 'utf8'); | ||
@@ -19,9 +21,8 @@ // Everything before options | ||
const package = require('../package'); | ||
console.log(`Horticulturalist ${package.version}`); | ||
info(`Horticulturalist ${package.version}`); | ||
}, | ||
outputHelp: () => { | ||
module.exports.outputVersion(); | ||
console.log(); | ||
console.log(pluckOptionsFromReadme()); | ||
info(pluckOptionsFromReadme()); | ||
} | ||
}; |
@@ -63,8 +63,2 @@ #!/usr/bin/env node | ||
if (active(argv.dev, argv.local, argv['medic-os'], argv.test).length !== 1) { | ||
help.outputHelp(); | ||
error('You must pick one mode to run in.'); | ||
process.exit(-1); | ||
} | ||
const mode = argv.dev ? MODES.development : | ||
@@ -76,3 +70,2 @@ argv.test ? MODES.test : | ||
if (argv.version || argv.v) { | ||
@@ -83,3 +76,3 @@ help.outputVersion(); | ||
if (!mode || argv.help || argv.h) { | ||
if (argv.help || argv.h) { | ||
help.outputHelp(); | ||
@@ -89,2 +82,8 @@ return; | ||
if (active(argv.dev, argv.local, argv['medic-os'], argv.test).length !== 1) { | ||
help.outputHelp(); | ||
error('You must pick one mode to run in.'); | ||
process.exit(-1); | ||
} | ||
if (active(argv.install, argv.stage, argv['complete-install']).length > 1) { | ||
@@ -108,3 +107,3 @@ help.outputHelp(); | ||
if (version === true) { | ||
version = 'medic:medic:master'; | ||
version = '@medic:medic:release'; | ||
} | ||
@@ -130,3 +129,3 @@ | ||
process.on('unhandledRejection', (err) => { | ||
console.error(err); | ||
error(err); | ||
fatality('Unhandled rejection, please raise this as a bug!'); | ||
@@ -133,0 +132,0 @@ }); |
@@ -18,2 +18,15 @@ const decompress = require('decompress'); | ||
const appNotCurrent = app => { | ||
const currentPath = app.deployPath('current'); | ||
if (!fs.existsSync(currentPath)) { | ||
return true; | ||
} | ||
const linkString = fs.readlinkSync(currentPath); | ||
if(!fs.existsSync(linkString)) { | ||
return true; | ||
} | ||
return linkString !== app.deployPath(); | ||
}; | ||
const getApps = () => { | ||
@@ -39,4 +52,4 @@ if (ddoc.node_modules) { | ||
debug(`Found ${JSON.stringify(changedApps)}`); | ||
changedApps = changedApps.filter(appNotAlreadyUnzipped); | ||
debug(`Apps that aren't unzipped: ${JSON.stringify(changedApps)}`); | ||
changedApps = changedApps.filter(appNotCurrent); | ||
debug(`Apps that are changed: ${JSON.stringify(changedApps)}`); | ||
@@ -46,4 +59,5 @@ return changedApps; | ||
const unzipChangedApps = (changedApps) => | ||
Promise.all(changedApps.map(app => { | ||
const unzipChangedApps = (changedApps) => { | ||
const appsToUnzip = changedApps.filter(appNotAlreadyUnzipped); | ||
return Promise.all(appsToUnzip.map(app => { | ||
const attachment = ddoc._attachments[app.attachmentName].data; | ||
@@ -57,2 +71,3 @@ return decompress(attachment, app.deployPath(), { | ||
})); | ||
}; | ||
@@ -59,0 +74,0 @@ return { |
@@ -95,3 +95,3 @@ const fs = require('fs-extra'), | ||
const deployStagedDdocs = () => { | ||
info(`Deploying staged ddocs`); | ||
info('Deploying staged ddocs'); | ||
@@ -118,3 +118,5 @@ return moduleWithContext._loadStagedDdocs() | ||
fs.symlinkSync(linkString, oldLinkString); | ||
} else debug(`Old app not found at ${linkString}.`); | ||
} else { | ||
debug(`Old app not found at ${linkString}.`); | ||
} | ||
@@ -121,0 +123,0 @@ fs.unlinkSync(livePath); |
@@ -1,12 +0,20 @@ | ||
const { info, debug, stage: stageLog } = require('../log'), | ||
const { info, debug, stage: stageLog, error } = require('../log'), | ||
DB = require('../dbs'), | ||
fs = require('fs-extra'), | ||
utils = require('../utils'), | ||
ddocWrapper = require('./ddocWrapper'); | ||
ddocWrapper = require('./ddocWrapper'), | ||
warmViews = require('./warmViews'); | ||
const ACTIVE_TASK_QUERY_INTERVAL = 10 * 1000; // 10 seconds | ||
const stager = deployDoc => (key, message) => { | ||
stageLog(message); | ||
return utils.appendDeployLog(deployDoc, {key: key, message: message}); | ||
const stageRunner = deployDoc => (key, message, stageFn) => { | ||
return utils.readyStage(deployDoc, key, message) | ||
.then(stageShouldRun => { | ||
if (stageFn && !stageShouldRun) { | ||
// Mark stages with executable content against them as skipped if we | ||
// don't think we should run them again | ||
stageLog(`Skipping: ${message}`); | ||
} else { | ||
stageLog(message); | ||
return stageFn && stageFn(); | ||
} | ||
}); | ||
}; | ||
@@ -20,2 +28,27 @@ | ||
const appId = deployDoc => `_design/${deployDoc.build_info.application}`; | ||
const findDownloadedBuild = deployDoc => { | ||
debug(`Locating already downloaded ${keyFromDeployDoc(deployDoc)}`); | ||
const id = utils.getStagedDdocId(appId(deployDoc)); | ||
return DB.app.get(id, { | ||
attachments: true, | ||
binary: true | ||
}) | ||
.catch(err => { | ||
// Two reasons this might be happening (as well as "CouchDB is down etc"): | ||
// - We are trying to `--complete-install` without `--stage`ing first, and so there is no | ||
// ddoc to pick up from. This is highly unlikely as we check for the deploy doc being in the | ||
// right state before getting here. | ||
// - This deploy failed on or after the staged ddocs are deleted. This is highly unlikely | ||
// because (as of writing) this is the very last stage-- postCleanup. | ||
// | ||
// The solution for both of these problems would be to start the installation again | ||
error(`Failed to find existing staged ddoc: ${err.message}`); | ||
throw err; | ||
}); | ||
}; | ||
const downloadBuild = deployDoc => { | ||
@@ -27,3 +60,3 @@ debug(`Downloading ${keyFromDeployDoc(deployDoc)}, this may take some time…`); | ||
deployable._id = `_design/${deployDoc.build_info.application}`; | ||
deployable._id = appId(deployDoc); | ||
utils.stageDdoc(deployable); | ||
@@ -64,137 +97,2 @@ deployable.deploy_info = { | ||
const warmViews = (deployDoc) => { | ||
let viewsWarmed = false; | ||
const writeProgress = () => { | ||
return DB.activeTasks() | ||
.then(tasks => { | ||
const relevantTasks = tasks.filter(task => | ||
task.type === 'indexer' && task.design_document.includes(':staged:')); | ||
return updateIndexers(relevantTasks); | ||
}); | ||
}; | ||
// logs indexer progress in the console | ||
// _design/doc [||||||||||29%||||||||||_________________________________________________________] | ||
const logIndexersProgress = (indexers) => { | ||
if (!indexers || !indexers.length) { | ||
return; | ||
} | ||
const logProgress = (indexer) => { | ||
// progress bar stretches to match console width. | ||
// 60 is roughly the nbr of chars displayed around the bar (ddoc name + debug padding) | ||
const barLength = process.stdout.columns - 60, | ||
progress = `${indexer.progress}%`, | ||
filledBarLength = (indexer.progress / 100 * barLength), | ||
bar = progress | ||
.padStart((filledBarLength + progress.length) / 2, '|') | ||
.padEnd(filledBarLength, '|') | ||
.padEnd(barLength, '_'), | ||
ddocName = indexer.design_document.padEnd(35, ' '); | ||
debug(`${ddocName}[${bar}]`); | ||
}; | ||
debug('View indexer progress'); | ||
indexers.forEach(logProgress); | ||
}; | ||
// Groups tasks by `design_document` and calculates the average progress per ddoc | ||
// When a task is finished, it disappears from _active_tasks | ||
const updateIndexers = (runningTasks) => { | ||
const entry = deployDoc.log[deployDoc.log.length - 1], | ||
indexers = entry.indexers || []; | ||
// We assume all previous tasks have finished. | ||
indexers.forEach(setTasksToComplete); | ||
// If a task is new or still running, it's progress is updated | ||
updateRunningTasks(indexers, runningTasks); | ||
indexers.forEach(calculateAverageProgress); | ||
entry.indexers = indexers; | ||
logIndexersProgress(indexers); | ||
return utils.update(deployDoc); | ||
}; | ||
const setTasksToComplete = (indexer) => { | ||
Object | ||
.keys(indexer.tasks) | ||
.forEach(pid => { | ||
indexer.tasks[pid] = 100; | ||
}); | ||
}; | ||
const calculateAverageProgress = (indexer) => { | ||
const tasks = Object.keys(indexer.tasks); | ||
indexer.progress = Math.round(tasks.reduce((progress, pid) => progress + indexer.tasks[pid], 0) / tasks.length); | ||
}; | ||
const updateRunningTasks = (indexers, activeTasks = []) => { | ||
activeTasks.forEach(task => { | ||
let indexer = indexers.find(indexer => indexer.design_document === task.design_document); | ||
if (!indexer) { | ||
indexer = { | ||
design_document: task.design_document, | ||
tasks: {}, | ||
}; | ||
indexers.push(indexer); | ||
} | ||
indexer.tasks[`${task.node}-${task.pid}`] = task.progress; | ||
}); | ||
}; | ||
// Query _active_tasks every 10 seconds until `viewsWarmed` is true | ||
const writeProgressTimeout = () => { | ||
setTimeout(() => { | ||
if (viewsWarmed) { | ||
return; | ||
} | ||
writeProgress().then(writeProgressTimeout); | ||
}, ACTIVE_TASK_QUERY_INTERVAL); | ||
}; | ||
const probeViews = viewlist => { | ||
return Promise | ||
.all(viewlist.map(view => DB.app.query(view, { limit: 1 }))) | ||
.then(() => { | ||
viewsWarmed = true; | ||
info('Warming views complete'); | ||
return updateIndexers(); | ||
}) | ||
.catch(err => { | ||
if (err.error !== 'timeout') { | ||
throw err; | ||
} | ||
return probeViews(viewlist); | ||
}); | ||
}; | ||
const firstView = ddoc => | ||
`${ddoc._id.replace('_design/', '')}/${Object.keys(ddoc.views).find(k => k !== 'lib')}`; | ||
return utils.getStagedDdocs(true) | ||
.then(ddocs => { | ||
debug(`Got ${ddocs.length} staged ddocs`); | ||
const queries = ddocs | ||
.filter(ddoc => ddoc.views && Object.keys(ddoc.views).length) | ||
.map(firstView); | ||
info('Beginning view warming'); | ||
deployDoc.log.push({ | ||
type: 'warm_log' | ||
}); | ||
return utils.update(deployDoc) | ||
.then(() => { | ||
writeProgressTimeout(); | ||
return probeViews(queries); | ||
}); | ||
}); | ||
}; | ||
const clearStagedDdocs = () => { | ||
@@ -222,3 +120,5 @@ debug('Clear existing staged DBs'); | ||
fs.removeSync(linkString); | ||
} else debug(`Old app not found at ${linkString}.`); | ||
} else { | ||
debug(`Old app not found at ${linkString}.`); | ||
} | ||
@@ -262,3 +162,3 @@ fs.unlinkSync(oldPath); | ||
const predeploySteps = (deployDoc) => { | ||
const stage = stager(deployDoc); | ||
const stage = stageRunner(deployDoc); | ||
@@ -268,11 +168,11 @@ let ddoc; | ||
return stage('horti.stage.init', `Horticulturalist deployment of '${keyFromDeployDoc(deployDoc)}' initialising`) | ||
.then(() => stage('horti.stage.preCleanup', 'Pre-deploy cleanup')) | ||
.then(() => preCleanup()) | ||
.then(() => stage('horti.stage.download', 'Downloading and staging install')) | ||
.then(() => downloadBuild(deployDoc)) | ||
.then(() => stage('horti.stage.preCleanup', 'Pre-deploy cleanup', preCleanup)) | ||
.then(() => stage('horti.stage.download', 'Downloading and staging install', () => downloadBuild(deployDoc))) | ||
.then(stagedDdoc => { | ||
// If we're resuming a deployment and we skip the above stage we need to find the ddoc manually | ||
return stagedDdoc || findDownloadedBuild(deployDoc); | ||
}) | ||
.then(stagedDdoc => ddoc = stagedDdoc) | ||
.then(() => stage('horti.stage.extractingDdocs', 'Extracting ddocs')) | ||
.then(() => extractDdocs(ddoc)) | ||
.then(() => stage('horti.stage.warmingViews', 'Warming views')) | ||
.then(() => warmViews(deployDoc)) | ||
.then(() => stage('horti.stage.extractingDdocs', 'Extracting ddocs', () => extractDdocs(ddoc))) | ||
.then(() => stage('horti.stage.warmingViews', 'Warming views', () => warmViews().warm(deployDoc))) | ||
.then(() => stage('horti.stage.readyToDeploy', 'View warming complete, ready to deploy')) | ||
@@ -290,20 +190,12 @@ .then(() => ddoc); | ||
} else { | ||
debug('Loading application ddoc'); | ||
const ddocId = utils.getStagedDdocId(`_design/${deployDoc.build_info.application}`); | ||
return DB.app.get(ddocId, { | ||
attachments: true, | ||
binary: true | ||
}); | ||
return findDownloadedBuild(deployDoc); | ||
} | ||
}; | ||
const stage = stager(deployDoc); | ||
const stage = stageRunner(deployDoc); | ||
return stage('horti.stage.initDeploy', 'Initiating deployment') | ||
.then(getApplicationDdoc) | ||
.then(ddoc => { | ||
return stage('horti.stage.deploying', 'Deploying new installation') | ||
.then(() => performDeploy(mode, deployDoc, ddoc, firstRun)) | ||
.then(() => stage('horti.stage.postCleanup', 'Post-deploy cleanup, installation complete')) | ||
.then(() => postCleanup(ddocWrapper(ddoc, mode), deployDoc)); | ||
}); | ||
.then(stagedDdoc => ddoc = stagedDdoc) | ||
.then(() => stage('horti.stage.deploying', 'Deploying new installation', () => performDeploy(mode, deployDoc, ddoc, firstRun))) | ||
.then(() => stage('horti.stage.postCleanup', 'Post-deploy cleanup, installation complete', () => postCleanup(ddocWrapper(ddoc, mode), deployDoc))); | ||
}; | ||
@@ -343,5 +235,4 @@ | ||
_extractDdocs: extractDdocs, | ||
_warmViews: warmViews, | ||
_deploySteps: deploySteps, | ||
_postCleanup: postCleanup | ||
}; |
@@ -14,6 +14,3 @@ const lockfile = require('lockfile'); | ||
new Promise((resolve, reject) => { | ||
lockfile.lock(LOCK_FILE, err => { | ||
if(err) reject(err); | ||
else resolve(); | ||
}); | ||
lockfile.lock(LOCK_FILE, err => err ? reject(err) : resolve()); | ||
}); | ||
@@ -20,0 +17,0 @@ |
@@ -5,4 +5,4 @@ const debug = require('debug'); | ||
module.exports.stage = debug('horti:stage'); | ||
module.exports.info = console.log; | ||
module.exports.error = console.error; | ||
module.exports.info = console.log; //eslint-disable-line | ||
module.exports.error = console.error; //eslint-disable-line | ||
@@ -9,0 +9,0 @@ if (!process.env.TESTING) { |
@@ -105,3 +105,3 @@ const { debug } = require('./log'); | ||
}, | ||
appendDeployLog: (deployDoc, message, type='stage') => { | ||
readyStage: (deployDoc, key, message) => { | ||
if (!deployDoc.log) { | ||
@@ -111,9 +111,25 @@ deployDoc.log = []; | ||
deployDoc.log.push({ | ||
type: type, | ||
datetime: new Date().getTime(), | ||
message: message | ||
}); | ||
const existingStages = deployDoc.log.filter(entry => entry.type === 'stage'); | ||
const thisStageIdx = existingStages.findIndex(entry => entry.key === key); | ||
return module.exports.update(deployDoc); | ||
if (thisStageIdx === -1) { | ||
// We have not attempted this stage before | ||
deployDoc.log.push({ | ||
type: 'stage', | ||
datetime: new Date().getTime(), | ||
key: key, | ||
message: { message, key } | ||
}); | ||
return module.exports.update(deployDoc) | ||
.then(() => true); | ||
} else if (thisStageIdx === existingStages.length - 1) { | ||
// Attempted and not passed, so try again | ||
return Promise.resolve(true); | ||
} else { | ||
// Attemped and passed, so skip | ||
return Promise.resolve(false); | ||
} | ||
}, | ||
@@ -141,3 +157,3 @@ update: doc => { | ||
if (err.code === 'EPIPE') { | ||
err.horticulturalist = `Failed to perform bulk docs, you may need to increase CouchDB's max_http_request_size`; | ||
err.horticulturalist = 'Failed to perform bulk docs, you may need to increase CouchDB\'s max_http_request_size'; | ||
throw err; | ||
@@ -144,0 +160,0 @@ } |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
61555
25
1274
1
70