254 lines
7.6 KiB
JavaScript
254 lines
7.6 KiB
JavaScript
|
|
const execFile = require('child_process').execFile;
|
|
const log = require('fancy-log');
|
|
const path = require('path');
|
|
|
|
module.exports = class Docker {
|
|
constructor(name, context) {
|
|
Object.assign(this, {
|
|
id: name,
|
|
name,
|
|
isRandom: name == null,
|
|
dbConf: {
|
|
host: 'localhost',
|
|
port: '3306',
|
|
username: 'root',
|
|
password: 'root'
|
|
},
|
|
imageTag: name || 'myvc/dump',
|
|
context
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Builds the database image and runs a container. It only rebuilds the
|
|
* image when fixtures have been modified or when the day on which the
|
|
* image was built is different to today. Some workarounds have been used
|
|
* to avoid a bug with OverlayFS driver on MacOS.
|
|
*
|
|
* @param {Boolean} ci continuous integration environment argument
|
|
*/
|
|
async run(ci) {
|
|
let dockerfilePath = path.join(__dirname, 'Dockerfile');
|
|
|
|
await this.execFile('docker', [
|
|
'build',
|
|
'-t', 'myvc/server',
|
|
'-f', `${dockerfilePath}.server`,
|
|
__dirname
|
|
]);
|
|
|
|
let d = new Date();
|
|
let pad = v => v < 10 ? '0' + v : v;
|
|
let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
|
|
await this.execFile('docker', [
|
|
'build',
|
|
'-t', this.imageTag,
|
|
'-f', `${dockerfilePath}.dump`,
|
|
'--build-arg', `STAMP=${stamp}`,
|
|
this.context
|
|
]);
|
|
|
|
let dockerArgs;
|
|
|
|
if (this.isRandom)
|
|
dockerArgs = ['-p', '3306'];
|
|
else {
|
|
try {
|
|
await this.rm();
|
|
} catch (e) {}
|
|
dockerArgs = ['--name', this.name, '-p', `3306:${this.dbConf.port}`];
|
|
}
|
|
|
|
let runChown = process.platform != 'linux';
|
|
const container = await this.execFile('docker', [
|
|
'run',
|
|
'--env', `RUN_CHOWN=${runChown}`,
|
|
'-d',
|
|
...dockerArgs,
|
|
this.imageTag
|
|
]);
|
|
this.id = container.stdout.trim();
|
|
|
|
try {
|
|
if (this.isRandom) {
|
|
let netSettings = await this.execJson('docker', [
|
|
'inspect', '-f', '{{json .NetworkSettings}}', this.id
|
|
]);
|
|
|
|
if (ci)
|
|
this.dbConf.host = netSettings.Gateway;
|
|
|
|
this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort'];
|
|
}
|
|
|
|
await this.wait();
|
|
} catch (err) {
|
|
if (this.isRandom)
|
|
await this.rm();
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Does the minium effort to start the database container, if it doesn't
|
|
* exists calls the 'docker' task, if it is started does nothing. Keep in
|
|
* mind that when you do not rebuild the docker you may be using an outdated
|
|
* version of it. See the 'docker' task for more info.
|
|
*/
|
|
async start() {
|
|
let state;
|
|
try {
|
|
state = await this.execJson('docker', [
|
|
'inspect', '-f', '{{json .State}}', this.id
|
|
]);
|
|
} catch (err) {
|
|
return await this.run();
|
|
}
|
|
|
|
switch (state.Status) {
|
|
case 'running':
|
|
return;
|
|
case 'exited':
|
|
await this.execFile('docker', ['start', this.id]);
|
|
await this.wait();
|
|
return;
|
|
default:
|
|
throw new Error(`Unknown docker status: ${state.Status}`);
|
|
}
|
|
}
|
|
|
|
waitForHealthy() {
|
|
return new Promise((resolve, reject) => {
|
|
let interval = 100;
|
|
let elapsedTime = 0;
|
|
let maxInterval = 4 * 60 * 1000;
|
|
|
|
log('Waiting for container to be ready...');
|
|
|
|
async function checker() {
|
|
elapsedTime += interval;
|
|
let status;
|
|
|
|
try {
|
|
let status = await this.execJson('docker', [
|
|
'inspect', '-f', '{{.State.Health.Status}}', this.id
|
|
]);
|
|
status = status.trimEnd();
|
|
} catch (err) {
|
|
return reject(new Error(err.message));
|
|
}
|
|
|
|
if (status == 'unhealthy')
|
|
return reject(new Error('Docker exited, please see the docker logs for more info'));
|
|
|
|
if (status == 'healthy') {
|
|
log('Container ready.');
|
|
return resolve();
|
|
}
|
|
|
|
if (elapsedTime >= maxInterval)
|
|
reject(new Error(`Container not initialized whithin ${elapsedTime / 1000} secs`));
|
|
else
|
|
setTimeout(bindedChecker, interval);
|
|
}
|
|
let bindedChecker = checker.bind(this);
|
|
bindedChecker();
|
|
});
|
|
}
|
|
|
|
wait() {
|
|
return new Promise((resolve, reject) => {
|
|
const mysql = require('mysql2');
|
|
|
|
let interval = 100;
|
|
let elapsedTime = 0;
|
|
let maxInterval = 4 * 60 * 1000;
|
|
|
|
let myConf = {
|
|
user: this.dbConf.username,
|
|
password: this.dbConf.password,
|
|
host: this.dbConf.host,
|
|
port: this.dbConf.port
|
|
};
|
|
|
|
log('Waiting for MySQL init process...');
|
|
|
|
async function checker() {
|
|
elapsedTime += interval;
|
|
let state;
|
|
|
|
try {
|
|
state = await this.execJson('docker', [
|
|
'inspect', '-f', '{{json .State}}', this.id
|
|
]);
|
|
} catch (err) {
|
|
return reject(new Error(err.message));
|
|
}
|
|
|
|
if (state.Status === 'exited')
|
|
return reject(new Error('Docker exited, please see the docker logs for more info'));
|
|
|
|
let conn = mysql.createConnection(myConf);
|
|
conn.on('error', () => {});
|
|
conn.connect(err => {
|
|
conn.destroy();
|
|
if (!err) {
|
|
log('MySQL process ready.');
|
|
return resolve();
|
|
}
|
|
|
|
if (elapsedTime >= maxInterval)
|
|
reject(new Error(`MySQL not initialized whithin ${elapsedTime / 1000} secs`));
|
|
else
|
|
setTimeout(bindedChecker, interval);
|
|
});
|
|
}
|
|
let bindedChecker = checker.bind(this);
|
|
bindedChecker();
|
|
});
|
|
}
|
|
|
|
async rm() {
|
|
try {
|
|
await this.execFile('docker', ['stop', this.id]);
|
|
await this.execFile('docker', ['rm', '-v', this.id]);
|
|
} catch (e) {}
|
|
}
|
|
|
|
/**
|
|
* Promisified version of execFile().
|
|
*
|
|
* @param {String} command The exec command
|
|
* @param {Array} args The command arguments
|
|
* @return {Promise} The promise
|
|
*/
|
|
execFile(command, args) {
|
|
return new Promise((resolve, reject) => {
|
|
execFile(command, args, (err, stdout, stderr) => {
|
|
if (err)
|
|
reject(err);
|
|
else {
|
|
resolve({
|
|
stdout: stdout,
|
|
stderr: stderr
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Executes a command whose return is json.
|
|
*
|
|
* @param {String} command The exec command
|
|
* @param {Array} args The command arguments
|
|
* @return {Object} The parsed JSON
|
|
*/
|
|
async execJson(command, args) {
|
|
const result = await this.execFile(command, args);
|
|
return JSON.parse(result.stdout);
|
|
}
|
|
};
|