myt/docker.js

217 lines
6.9 KiB
JavaScript

const cwd = process.cwd();
const exec = require('child_process').exec;
const log = require('fancy-log');
const path = require('path');
const serverImage = require(`${cwd}/myvc.config.json`).serverImage;
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.execP(`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.execP(`docker build --build-arg STAMP=${stamp} -f ${dockerfilePath}.dump -t ${this.serverImage} ${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.execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} ${this.serverImage}`);
this.id = container.stdout.trim();
try {
if (this.isRandom) {
let inspect = await this.execP(`docker inspect -f "{{json .NetworkSettings}}" ${this.id}`);
let netSettings = JSON.parse(inspect.stdout);
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 {
let result = await this.execP(`docker inspect -f "{{json .State}}" ${this.id}`);
state = JSON.parse(result.stdout);
} catch (err) {
return await this.run();
}
switch (state.Status) {
case 'running':
return;
case 'exited':
await this.execP(`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 MySQL init process...');
async function checker() {
elapsedTime += interval;
let status;
try {
let result = await this.execP(`docker inspect -f "{{.State.Health.Status}}" ${this.id}`);
status = result.stdout.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('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();
});
}
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 {
let result = await this.execP(`docker inspect -f "{{json .State}}" ${this.id}`);
state = JSON.parse(result.stdout);
} 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();
});
}
rm() {
return this.execP(`docker stop ${this.id} && docker rm -v ${this.id}`);
}
/**
* Promisified version of exec().
*
* @param {String} command The exec command
* @return {Promise} The promise
*/
execP(command) {
return new Promise((resolve, reject) => {
exec(command, (err, stdout, stderr) => {
if (err)
reject(err);
else {
resolve({
stdout: stdout,
stderr: stderr
});
}
});
});
}
};