couch-continuum
Advanced tools
Comparing version 2.2.3 to 2.2.4
261
bin.js
@@ -54,14 +54,61 @@ #!/usr/bin/env node | ||
function catchError (error) { | ||
console.log('ERROR') | ||
if (error.error === 'not_found') { | ||
console.log('Primary database does not exist. There is nothing to migrate.') | ||
} else if (error.error === 'unauthorized') { | ||
console.log('Could not authenticate with CouchDB. Are the credentials correct?') | ||
} else if (error.code === 'EACCES') { | ||
console.log('Could not access the checkpoint document. Are you running as a different user?') | ||
} else { | ||
console.log('Unexpected error: %j', error) | ||
} | ||
process.exit(1) | ||
function generalOptions (yargs) { | ||
return yargs | ||
// backwards compat with old flag names | ||
.alias('source', 'dbNames') | ||
.alias('source', 'N') | ||
.alias('target', 'copyName') | ||
.alias('target', 'c') | ||
// actual options | ||
.options({ | ||
source: { | ||
alias: 's', | ||
description: 'The name or URL of a database to use as a primary.', | ||
required: true, | ||
type: 'string' | ||
}, | ||
target: { | ||
alias: 't', | ||
description: 'The name or URL of a database to use as a replica. Defaults to {source}_temp_copy', | ||
type: 'string' | ||
}, | ||
couchUrl: { | ||
alias: 'u', | ||
description: 'The URL of the CouchDB cluster to act upon.', | ||
default: process.env.COUCH_URL || 'http://localhost:5984' | ||
}, | ||
interval: { | ||
alias: 'i', | ||
description: 'How often (in milliseconds) to check replication tasks for progress.', | ||
default: 1000 | ||
}, | ||
q: { | ||
description: 'The desired "q" value for the new database.', | ||
type: 'number' | ||
}, | ||
n: { | ||
description: 'The desired "n" value for the new database.', | ||
type: 'number' | ||
}, | ||
verbose: { | ||
alias: 'v', | ||
description: 'Enable verbose logging.', | ||
type: 'boolean' | ||
}, | ||
placement: { | ||
alias: 'p', | ||
description: 'Placement rule for the affected database(s).', | ||
type: 'string' | ||
}, | ||
filterTombstones: { | ||
alias: 'f', | ||
description: 'Filter tombstones during replica creation. Does not work with CouchDB 1.x', | ||
default: false | ||
}, | ||
replicateSecurity: { | ||
alias: 'r', | ||
description: 'Replicate a database\'s /_security object in addition to its documents.', | ||
default: true | ||
} | ||
}) | ||
} | ||
@@ -78,12 +125,17 @@ | ||
description: 'Migrate a database to new settings.', | ||
builder: generalOptions, | ||
handler: async function (argv) { | ||
const continuum = getContinuum(argv) | ||
log(`Migrating database: ${continuum.source.host}${continuum.source.path}`) | ||
try { | ||
await continuum.createReplica() | ||
const consent = await getConsent() | ||
if (!consent) return log('Could not acquire consent. Exiting...') | ||
await continuum.replacePrimary() | ||
console.log(`Migrated database: ${continuum.source.host}${continuum.source.path}`) | ||
} catch (error) { catchError(error) } | ||
log(`Migrating database: ${continuum.source.host}${continuum.source.pathname}`) | ||
await continuum.createReplica() | ||
const consent1 = await getConsent() | ||
if (!consent1) return log('Could not acquire consent. Exiting...') | ||
await continuum.replacePrimary() | ||
console.log(`Migrated database: ${continuum.source.host}${continuum.source.pathname}`) | ||
log(`Migrating database: ${continuum.source.host}${continuum.source.pathname}`) | ||
await continuum.createReplica() | ||
const consent2 = await getConsent() | ||
if (!consent2) return log('Could not acquire consent. Exiting...') | ||
await continuum.replacePrimary() | ||
console.log(`Migrated database: ${continuum.source.host}${continuum.source.pathname}`) | ||
} | ||
@@ -95,9 +147,11 @@ }) | ||
description: 'Create a replica of the given primary.', | ||
builder: generalOptions, | ||
handler: async function (argv) { | ||
const continuum = getContinuum(argv) | ||
log(`Creating replica of ${continuum.source.host}${continuum.source.path} at ${continuum.target.host}${continuum.target.path}`) | ||
try { | ||
await continuum.createReplica() | ||
console.log(`Created replica of ${continuum.source.host}${continuum.source.path}`) | ||
} catch (error) { catchError(error) } | ||
log(`Creating replica of ${continuum.source.host}${continuum.source.pathname} at ${continuum.target.host}${continuum.target.pathname}`) | ||
await continuum.createReplica() | ||
console.log(`Created replica of ${continuum.source.host}${continuum.source.pathname}`) | ||
log(`Creating replica of ${continuum.source.host}${continuum.source.pathname} at ${continuum.target.host}${continuum.target.pathname}`) | ||
await continuum.createReplica() | ||
console.log(`Created replica of ${continuum.source.host}${continuum.source.pathname}`) | ||
} | ||
@@ -109,11 +163,10 @@ }) | ||
description: 'Replace the given primary with the indicated replica.', | ||
builder: generalOptions, | ||
handler: async function (argv) { | ||
const continuum = getContinuum(argv) | ||
log(`Replacing primary ${continuum.source.host}${continuum.source.path} with ${continuum.target.host}${continuum.target.path}`) | ||
try { | ||
const consent = await getConsent() | ||
if (!consent) return log('Could not acquire consent. Exiting...') | ||
await continuum.replacePrimary() | ||
console.log(`Successfully replaced ${continuum.source.host}${continuum.source.path}`) | ||
} catch (error) { catchError(error) } | ||
log(`Replacing primary ${continuum.source.host}${continuum.source.pathname} with ${continuum.target.host}${continuum.target.pathname}`) | ||
const consent = await getConsent() | ||
if (!consent) return log('Could not acquire consent. Exiting...') | ||
await continuum.replacePrimary() | ||
console.log(`Successfully replaced ${continuum.source.host}${continuum.source.pathname}`) | ||
} | ||
@@ -125,80 +178,82 @@ }) | ||
description: 'Migrate all non-special databases to new settings.', | ||
builder: function (yargs) { | ||
yargs.options({ | ||
couchUrl: { | ||
alias: 'u', | ||
description: 'The URL of the CouchDB cluster to act upon.', | ||
default: process.env.COUCH_URL || 'http://localhost:5984' | ||
}, | ||
interval: { | ||
alias: 'i', | ||
description: 'How often (in milliseconds) to check replication tasks for progress.', | ||
default: 1000 | ||
}, | ||
q: { | ||
description: 'The desired "q" value for the new database.', | ||
type: 'number' | ||
}, | ||
n: { | ||
description: 'The desired "n" value for the new database.', | ||
type: 'number' | ||
}, | ||
verbose: { | ||
alias: 'v', | ||
description: 'Enable verbose logging.', | ||
type: 'boolean' | ||
}, | ||
placement: { | ||
alias: 'p', | ||
description: 'Placement rule for the affected database(s).', | ||
type: 'string' | ||
}, | ||
filterTombstones: { | ||
alias: 'f', | ||
description: 'Filter tombstones during replica creation. Does not work with CouchDB 1.x', | ||
default: false | ||
}, | ||
replicateSecurity: { | ||
alias: 'r', | ||
description: 'Replicate a database\'s /_security object in addition to its documents.', | ||
default: true | ||
} | ||
}) | ||
}, | ||
handler: async function (argv) { | ||
const { couchUrl, verbose } = argv | ||
if (verbose) { process.env.LOG = true } | ||
try { | ||
const dbNames = await CouchContinuum.getRemaining(couchUrl) | ||
const continuums = dbNames.map((dbName) => { | ||
return new CouchContinuum({ dbName, ...argv }) | ||
}) | ||
log('Creating replicas...') | ||
await CouchContinuum.createReplicas(continuums) | ||
const consent = await getConsent('Ready to replace primaries with replicas. Continue? [y/N] ') | ||
if (!consent) return console.log('Could not acquire consent. Exiting...') | ||
log('Replacing primaries...') | ||
await CouchContinuum.replacePrimaries(continuums) | ||
await CouchContinuum.removeCheckpoint() | ||
console.log(`Successfully migrated databases: ${dbNames.join(', ')}`) | ||
} catch (error) { catchError(error) } | ||
const dbNames = await CouchContinuum.getRemaining(couchUrl) | ||
if (!dbNames.length) { | ||
console.log('No eligible databases to migrate.') | ||
return | ||
} | ||
const continuums = dbNames.map((source) => { | ||
return new CouchContinuum({ source, ...argv }) | ||
}) | ||
log('Creating replicas...') | ||
await CouchContinuum.createReplicas(continuums) | ||
const consent = await getConsent('Ready to replace primaries with replicas. Continue? [y/N] ') | ||
if (!consent) return console.log('Could not acquire consent. Exiting...') | ||
log('Replacing primaries...') | ||
await CouchContinuum.replacePrimaries(continuums) | ||
await CouchContinuum.removeCheckpoint() | ||
console.log(`Successfully migrated databases: ${dbNames.join(', ')}`) | ||
} | ||
}) | ||
// backwards compat with old flag names | ||
.alias('source', 'dbNames') | ||
.alias('source', 'N') | ||
.alias('target', 'copyName') | ||
.alias('target', 'c') | ||
// actual options | ||
.options({ | ||
source: { | ||
alias: 's', | ||
description: 'The name or URL of a database to use as a primary.', | ||
required: true, | ||
type: 'string' | ||
}, | ||
target: { | ||
alias: 't', | ||
description: 'The name or URL of a database to use as a replica. Defaults to {source}_temp_copy', | ||
type: 'string' | ||
}, | ||
couchUrl: { | ||
alias: 'u', | ||
description: 'The URL of the CouchDB cluster to act upon.', | ||
default: process.env.COUCH_URL || 'http://localhost:5984' | ||
}, | ||
interval: { | ||
alias: 'i', | ||
description: 'How often (in milliseconds) to check replication tasks for progress.', | ||
default: 1000 | ||
}, | ||
q: { | ||
description: 'The desired "q" value for the new database.', | ||
type: 'number' | ||
}, | ||
n: { | ||
description: 'The desired "n" value for the new database.', | ||
type: 'number' | ||
}, | ||
verbose: { | ||
alias: 'v', | ||
description: 'Enable verbose logging.', | ||
type: 'boolean' | ||
}, | ||
placement: { | ||
alias: 'p', | ||
description: 'Placement rule for the affected database(s).', | ||
type: 'string' | ||
}, | ||
filterTombstones: { | ||
alias: 'f', | ||
description: 'Filter tombstones during replica creation. Does not work with CouchDB 1.x', | ||
default: false | ||
}, | ||
replicateSecurity: { | ||
alias: 'r', | ||
description: 'Replicate a database\'s /_security object in addition to its documents.', | ||
default: true | ||
.config() | ||
.alias('h', 'help') | ||
.fail((msg, error, yargs) => { | ||
if (!error) { | ||
console.log(msg) | ||
} else if (error.error === 'not_found') { | ||
console.log('Primary database does not exist. There is nothing to migrate.') | ||
} else if (error.error === 'unauthorized') { | ||
console.log('Could not authenticate with CouchDB. Are the credentials correct?') | ||
} else if (error.code === 'EACCES') { | ||
console.log('Could not access the checkpoint document. Are you running as a different user?') | ||
} else { | ||
console.log('Unexpected error. Please report this so we can fix it!') | ||
console.log(error) | ||
} | ||
process.exit(1) | ||
}) | ||
.config() | ||
.alias('h', 'help') | ||
.parse() |
120
index.js
@@ -1,2 +0,2 @@ | ||
const assert = require('assert') | ||
const assert = require('assert').strict | ||
const path = require('path') | ||
@@ -59,3 +59,10 @@ const ProgressBar = require('progress') | ||
static async removeCheckpoint () { | ||
await unlink(checkpoint) | ||
try { | ||
await unlink(checkpoint) | ||
} catch (error) { | ||
// don't complain if checkpoint is already missing | ||
if (error.code !== 'ENOENT') { | ||
throw error | ||
} | ||
} | ||
} | ||
@@ -86,15 +93,32 @@ | ||
static async _isAvailable (dbUrl) { | ||
const { down } = await request({ | ||
url: `${dbUrl}/_local/in-maintenance`, | ||
json: true | ||
}) | ||
return !down | ||
try { | ||
const { down } = await request({ | ||
url: `${dbUrl}/_local/in-maintenance`, | ||
json: true | ||
}) | ||
return !down | ||
} catch (error) { | ||
if (error.error !== 'not_found') { | ||
throw error | ||
} else { | ||
// document doesn't exist, so, db must be available | ||
return true | ||
} | ||
} | ||
} | ||
static async _setUnavailable (dbUrl) { | ||
await request({ | ||
url: `${dbUrl}/_local/in-maintenance`, | ||
method: 'PUT', | ||
json: { down: true } | ||
}) | ||
const url = `${dbUrl}/_local/in-maintenance` | ||
try { | ||
// update | ||
const { _rev: rev } = await request({ url, json: true }) | ||
return request({ url, json: { _rev: rev, down: true }, method: 'PUT' }) | ||
} catch (error) { | ||
if (error.error === 'not_found') { | ||
// create | ||
await request({ url, method: 'PUT', json: { down: true } }) | ||
} else { | ||
throw error | ||
} | ||
} | ||
} | ||
@@ -104,4 +128,13 @@ | ||
const url = `${dbUrl}/_local/in-maintenance` | ||
const { _rev: rev } = await request({ url, json: true }) | ||
return request({ url, qs: { rev }, method: 'DELETE' }) | ||
try { | ||
const { _rev: rev } = await request({ url, json: true }) | ||
return request({ url, qs: { rev }, method: 'DELETE' }) | ||
} catch (error) { | ||
if (error.error === 'not_found') { | ||
// already available, nothing to do | ||
return null | ||
} else { | ||
throw error | ||
} | ||
} | ||
} | ||
@@ -172,8 +205,15 @@ | ||
if (this.placement) { qs.placement = this.placement } | ||
return request({ | ||
url: dbUrl, | ||
method: 'PUT', | ||
qs, | ||
json: true | ||
}) | ||
try { | ||
const result = await request({ | ||
url: dbUrl, | ||
method: 'PUT', | ||
qs, | ||
json: true | ||
}) | ||
return result | ||
} catch (error) { | ||
if (error.error !== 'file_exists') { | ||
throw error | ||
} | ||
} | ||
} | ||
@@ -264,14 +304,28 @@ | ||
// TODO check all known hosts | ||
const activeTasks = await request({ | ||
url: `${this.url.href}_active_tasks`, | ||
json: true | ||
}) | ||
const { jobs } = await request({ | ||
url: `${this.url.href}_scheduler/jobs`, | ||
json: true | ||
}).then(({ jobs }) => { | ||
return { jobs: jobs || [] } | ||
}) | ||
for (const { database } of [...jobs, ...activeTasks]) { | ||
assert.notStrictEqual(database, dbName, `${dbName} is still in use.`) | ||
try { | ||
const activeTasks = await request({ | ||
url: `${this.url.href}_active_tasks`, | ||
json: true | ||
}) | ||
const { jobs } = await request({ | ||
url: `${this.url.href}_scheduler/jobs`, | ||
json: true | ||
}).then(({ jobs }) => { | ||
return { jobs: jobs || [] } | ||
}) | ||
for (const { database, source, target } of [...jobs, ...activeTasks]) { | ||
const re = new RegExp(`/${dbName}/`) | ||
if (database) { | ||
assert.strictEqual(re.test(database), true) | ||
} | ||
assert.strictEqual(re.test(source), true) | ||
assert.strictEqual(re.test(target), true) | ||
} | ||
} catch (error) { | ||
if (error.error === 'illegal_database_name') { | ||
// 1.x -- this block is just for travis' test conditions | ||
return null | ||
} else { | ||
throw error | ||
} | ||
} | ||
@@ -306,3 +360,3 @@ } | ||
async replacePrimary () { | ||
log(`Replacing primary ${this.source.host}${this.source.pathname} using ${this.target.host}${this.target.path}...`) | ||
log(`Replacing primary ${this.source.host}${this.source.pathname} using ${this.target.host}${this.target.pathname}...`) | ||
log('[0/8] Checking if primary is in use...') | ||
@@ -309,0 +363,0 @@ await this._isInUse(this.source.pathname.slice(1)) |
@@ -7,5 +7,21 @@ const request = require('request') | ||
if (err) return reject(err) | ||
return resolve(body) | ||
if (res.statusCode >= 400) { | ||
let string, json | ||
if (typeof body === 'string') { | ||
string = body | ||
json = { options, ...JSON.parse(body) } | ||
} else { | ||
string = JSON.stringify(body) | ||
json = { options, ...body } | ||
} | ||
const error = new Error(string) | ||
Object.entries(json).map(([prop, value]) => { | ||
error[prop] = value | ||
}) | ||
return reject(error) | ||
} else { | ||
return resolve(body) | ||
} | ||
}) | ||
}) | ||
} |
{ | ||
"name": "couch-continuum", | ||
"version": "2.2.3", | ||
"version": "2.2.4", | ||
"description": "Tool for migrating CouchDB databases to new configuration values.", | ||
@@ -10,3 +10,3 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "standard && dependency-check . --unused --no-dev && mocha && npm audit" | ||
"test": "standard && dependency-check . --unused --no-dev && LOG=true mocha && npm audit" | ||
}, | ||
@@ -17,3 +17,3 @@ "author": "Diana Thayer <garbados@gmail.com>", | ||
"progress": "^2.0.3", | ||
"request": "^2.88.0", | ||
"request": "^2.88.2", | ||
"yargs": "^15.3.1" | ||
@@ -23,4 +23,4 @@ }, | ||
"dependency-check": "^4.1.0", | ||
"mocha": "^7.1.1", | ||
"standard": "^14.3.3" | ||
"mocha": "^8.0.1", | ||
"standard": "^14.3.4" | ||
}, | ||
@@ -27,0 +27,0 @@ "repository": { |
@@ -169,2 +169,2 @@ # couch-continuum | ||
(c) 2018 Neighbourhoodie Software & Open Source contributors | ||
(c) 2018–2020 Neighbourhoodie Software & Open Source contributors |
@@ -1,4 +0,4 @@ | ||
/* globals describe, it, beforeEach, before, afterEach */ | ||
/* globals describe, it, beforeEach, before, afterEach, after */ | ||
const assert = require('assert') | ||
const assert = require('assert').strict | ||
const CouchContinuum = require('..') | ||
@@ -21,4 +21,10 @@ const request = require('../lib/request') | ||
// ensure db exists | ||
const url = [couchUrl, dbName].join('/') | ||
await request({ url, method: 'PUT' }) | ||
const url = `${couchUrl}/${dbName}` | ||
try { | ||
await request({ url, method: 'PUT' }) | ||
} catch (error) { | ||
if (error.error !== 'file_exists') { | ||
throw error | ||
} | ||
} | ||
await request({ | ||
@@ -37,4 +43,14 @@ url: [url, '_bulk_docs'].join('/'), | ||
// destroy dbs | ||
await request({ url: `${couchUrl}/${dbName}`, method: 'DELETE' }) | ||
await request({ url: `${couchUrl}/temp_copy_${dbName}`, method: 'DELETE' }) | ||
const urls = ['', 'temp_copy_'].map((s) => { | ||
return `${couchUrl}/${s}${dbName}` | ||
}) | ||
for (const url of urls) { | ||
try { | ||
await request({ url, method: 'DELETE' }) | ||
} catch (error) { | ||
if (error.error !== 'not_found') { | ||
throw error | ||
} | ||
} | ||
} | ||
}) | ||
@@ -70,4 +86,7 @@ | ||
// verify cleanup | ||
const { error } = await request({ url: `${couchUrl}/temp_copy_${dbName}`, json: true }) | ||
assert.strictEqual(error, 'not_found') | ||
try { | ||
await request({ url: `${couchUrl}/temp_copy_${dbName}`, json: true }) | ||
} catch (error) { | ||
assert.strictEqual(error.error, 'not_found') | ||
} | ||
}) | ||
@@ -188,2 +207,44 @@ | ||
}) | ||
it('should run timer code', async function () { | ||
const interval = 1 | ||
const continuum = new CouchContinuum({ | ||
couchUrl, | ||
source: dbName, | ||
interval | ||
}) | ||
await continuum.createReplica() | ||
}) | ||
it('should handle removing a checkpoint more than once', async function () { | ||
const checkpoint = '.testcheckpoint' | ||
await CouchContinuum.makeCheckpoint(checkpoint) | ||
await CouchContinuum.removeCheckpoint(checkpoint) | ||
await CouchContinuum.removeCheckpoint(checkpoint) | ||
}) | ||
describe('_isInUse', function () { | ||
before(async function () { | ||
this.continuum = new CouchContinuum({ couchUrl, source: dbName }) | ||
await this.continuum._createDb(this.continuum.source.href) | ||
await this.continuum._createDb(this.continuum.target.href) | ||
await request({ | ||
method: 'POST', | ||
url: `${this.continuum.url.href}_replicate`, | ||
json: { | ||
continuous: true, | ||
source: this.continuum.source.href, | ||
target: this.continuum.target.href | ||
} | ||
}) | ||
}) | ||
it('should check for databases in use', function () { | ||
assert.rejects(this.continuum._isInUse(dbName)) | ||
}) | ||
after(async function () { | ||
await this.promise | ||
}) | ||
}) | ||
}) |
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
52171
875
8
Updatedrequest@^2.88.2