const exec = require('child_process').exec; const log = require('fancy-log'); const dataSources = require('../loopback/server/datasources.json'); module.exports = class Docker { constructor(name) { Object.assign(this, { id: name, name, isRandom: name == null, dbConf: Object.assign({}, dataSources.vn) }); } /** * 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 d = new Date(); let pad = v => v < 10 ? '0' + v : v; let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; log('Building container image...'); await this.execP(`docker build --build-arg STAMP=${stamp} -t salix-db ./db`); log('Image built.'); 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'; log('Starting container...'); const container = await this.execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} salix-db`); 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, connectTimeout: maxInterval }; 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 }); } }); }); } };