loopback-connector-couchdb2
Version:
LoopBack Connector for CouchDB 2.0
221 lines (200 loc) • 6.33 kB
JavaScript
// Copyright IBM Corp. 2017,2019. All Rights Reserved.
// Node module: loopback-connector-couchdb2
// This file is licensed under the Apache License 2.0.
// License text available at https://opensource.org/licenses/Apache-2.0
;
var _ = require('lodash');
var async = require('async');
var spawn = require('child_process').spawn;
var docker = new require('dockerode')();
var fmt = require('util').format;
var http = require('http');
var ms = require('ms');
// we don't pass any node flags, so we can call _mocha instead the wrapper
var mochaBin = require.resolve('mocha/bin/_mocha');
process.env.COUCHDB_DATABASE = 'test-db';
process.env.COUCHDB_PASSWORD = 'pass';
process.env.COUCHDB_USERNAME = 'admin';
// these are placeholders. They get set dynamically based on what IP and port
// get assigned by docker.
process.env.COUCHDB_PORT = 'TBD';
process.env.COUCHDB_HOST = 'TBD';
process.env.COUCHDB_URL = 'TBD';
var CONNECT_RETRIES = 30;
var CONNECT_DELAY = ms('5s');
var containerToDelete = null;
async.waterfall([
dockerStart('klaemo/couchdb:2.0.0'),
sleep(ms('2s')),
setCouchDBEnv,
waitFor('/_all_dbs'),
createDB('test-db'),
run([mochaBin, '--timeout', '40000', '--require', 'strong-mocha-interfaces', '--require', 'test/init.js', '--ui', 'strong-bdd']),
], function(testErr) {
dockerCleanup(function(cleanupErr) {
if (cleanupErr) {
console.error('error cleaning up:', cleanupErr);
}
if (testErr) {
console.error('error running tests:', testErr);
process.exit(1);
}
});
});
function sleep(n) {
return function delayedPassThrough() {
var args = [].slice.call(arguments);
// last argument is the callback
var next = args.pop();
// prepend `null` to indicate no error
args.unshift(null);
setTimeout(function() {
next.apply(null, args);
}, n);
};
}
function dockerStart(imgName) {
return function pullAndStart(next) {
console.log('pulling image: %s', imgName);
docker.pull(imgName, function(err, stream) {
docker.modem.followProgress(stream, function(err, output) {
if (err) {
return next(err);
}
console.log('starting container from image: %s', imgName);
docker.createContainer({
Image: imgName,
HostConfig: {
PublishAllPorts: true,
},
Env: [
'COUCHDB_USER=' + process.env.COUCHDB_USERNAME,
'COUCHDB_PASSWORD=' + process.env.COUCHDB_PASSWORD,
],
}, function(err, container) {
console.log('recording container for later cleanup: ', container.id);
containerToDelete = container;
if (err) {
return next(err);
}
container.start(function(err, data) {
next(err, container);
});
});
});
});
};
}
function setCouchDBEnv(container, next) {
container.inspect(function(err, c) {
// if swarm, Node.Ip will be set to actual node's IP
// if not swarm, but remote docker, use docker host's IP
// if local docker, use localhost
var host = _.get(c, 'Node.IP', _.get(docker, 'modem.host', '127.0.0.1'));
// container's port 80 is dynamically mapped to an external port
var port = _.get(c,
['NetworkSettings', 'Ports', '5984/tcp', '0', 'HostPort']);
process.env.COUCHDB_PORT = port;
process.env.COUCHDB_HOST = host;
var usr = process.env.COUCHDB_USERNAME;
var pass = process.env.COUCHDB_PASSWORD;
process.env.COUCHDB_URL = 'http://' + usr + ':' + pass + '@' +
host + ':' + port;
console.log('env:', _.pick(process.env, [
'COUCHDB_URL',
'COUCHDB_HOST',
'COUCHDB_PORT',
'COUCHDB_USERNAME',
'COUCHDB_PASSWORD',
'COUCHDB_DATABASE',
]));
next(null, container);
});
}
function waitFor(path) {
return function waitForPath(container, next) {
var opts = {
host: process.env.COUCHDB_HOST,
port: process.env.COUCHDB_PORT,
auth: process.env.COUCHDB_USERNAME + ':' + process.env.COUCHDB_PASSWORD,
path: path,
};
console.log('waiting for instance to respond');
return ping(null, CONNECT_RETRIES);
function ping(err, tries) {
console.log('ping (%d/%d)', CONNECT_RETRIES - tries, CONNECT_RETRIES);
if (tries < 1) {
next(err || new Error('failed to contact CouchDB'));
}
http.get(opts, function(res) {
res.pipe(devNull());
res.on('error', tryAgain);
res.on('end', function() {
if (res.statusCode === 200) {
setImmediate(next, null, container);
} else {
tryAgain();
}
});
}).on('error', tryAgain);
function tryAgain(err) {
setTimeout(ping, CONNECT_DELAY, err, tries - 1);
}
}
};
}
function createDB(db) {
return function create(container, next) {
var opts = {
method: 'PUT',
path: '/' + db,
host: process.env.COUCHDB_HOST,
port: process.env.COUCHDB_PORT,
auth: process.env.COUCHDB_USERNAME + ':' + process.env.COUCHDB_PASSWORD,
};
console.log('creating db: %j', db);
http.request(opts, function(res) {
res.pipe(devNull());
res.on('error', next);
res.on('end', function() {
setImmediate(next, null, container);
});
})
.on('error', next)
.end();
};
}
function run(cmd) {
return function spawnNode(container, next) {
spawn(process.execPath, cmd, {stdio: 'inherit'})
.on('error', next)
.on('exit', onExit);
function onExit(code, sig) {
if (code) {
next(new Error(fmt('mocha exited with code: %j, sig: %j', code, sig)));
} else {
next();
}
}
};
}
// clean up any previous containers
function dockerCleanup(next) {
if (containerToDelete) {
console.log('cleaning up container: %s', containerToDelete.id);
containerToDelete.remove({force: true}, function(err) {
next(err);
});
} else {
setImmediate(next);
}
}
// A Writable Stream that just consumes a stream. Useful for draining readable
// streams so that they 'end' properly, like sometimes-empty http responses.
function devNull() {
return new require('stream').Writable({
write: function(_chunk, _encoding, cb) {
return cb(null);
},
});
}