myt/docker.js

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);
}
};