Refactor and fixes

This commit is contained in:
Juan Ferrer 2020-11-16 14:23:28 +01:00
parent 8d825c4284
commit 513862648d
8 changed files with 238 additions and 139 deletions

View File

@ -212,7 +212,7 @@ applyRoutines() {
ROUTINE_TYPE=${SPLIT[1]} ROUTINE_TYPE=${SPLIT[1]}
case "$ROUTINE_TYPE" in case "$ROUTINE_TYPE" in
events) events)
ROUTINE_TYPE=EVENTS ROUTINE_TYPE=EVENT
;; ;;
functions) functions)
ROUTINE_TYPE=FUNCTION ROUTINE_TYPE=FUNCTION

View File

@ -1,15 +1,22 @@
#!/bin/node
const execFileSync = require('child_process').execFileSync; const path = require('path');
const execFile = require('child_process').execFile;
const spawn = require('child_process').spawn; const spawn = require('child_process').spawn;
module.exports = function(command, workdir, ...args) { module.exports = async function(command, workdir, ...args) {
const buildArgs = [ const buildArgs = [
'build', 'build',
'-t', 'myvc/client', '-t', 'myvc/client',
'-f', `${__dirname}/Dockerfile.client`, '-f', path.join(__dirname, 'Dockerfile.client'),
`${__dirname}/` __dirname
]; ];
execFileSync('docker', buildArgs); await new Promise((resolve, reject) => {
execFile('docker', buildArgs, (err, stdout, stderr) => {
if (err)
return reject(err);
resolve({stdout, stderr});
});
})
let runArgs = [ let runArgs = [
'run', 'run',
@ -19,12 +26,14 @@ module.exports = function(command, workdir, ...args) {
]; ];
runArgs = runArgs.concat(args); runArgs = runArgs.concat(args);
const child = spawn('docker', runArgs, { await new Promise((resolve, reject) => {
stdio: [ const child = spawn('docker', runArgs, {
process.stdin, stdio: [
process.stdout, process.stdin,
process.stderr process.stdout,
] process.stderr
}); ]
child.on('exit', code => process.exit(code)); });
child.on('exit', code => resolve(code));
})
}; };

View File

@ -1,9 +1,7 @@
const cwd = process.cwd(); const execFile = require('child_process').execFile;
const exec = require('child_process').exec;
const log = require('fancy-log'); const log = require('fancy-log');
const path = require('path'); const path = require('path');
const serverImage = require(`${cwd}/myvc.config.json`).serverImage;
module.exports = class Docker { module.exports = class Docker {
constructor(name, context) { constructor(name, context) {
@ -32,33 +30,52 @@ module.exports = class Docker {
*/ */
async run(ci) { async run(ci) {
let dockerfilePath = path.join(__dirname, 'Dockerfile'); let dockerfilePath = path.join(__dirname, 'Dockerfile');
await this.execP(`docker build -t myvc/server -f ${dockerfilePath}.server ${__dirname}`);
await this.execFile('docker', [
'build',
'-t', 'myvc/server',
'-f', `${dockerfilePath}.server`,
__dirname
]);
let d = new Date(); let d = new Date();
let pad = v => v < 10 ? '0' + v : v; let pad = v => v < 10 ? '0' + v : v;
let stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; 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}`);
await this.execFile('docker', [
'build',
'-t', this.imageTag,
'-f', `${dockerfilePath}.dump`,
'--build-arg', `STAMP=${stamp}`,
this.context
]);
let dockerArgs; let dockerArgs;
if (this.isRandom) if (this.isRandom)
dockerArgs = '-p 3306'; dockerArgs = ['-p', '3306'];
else { else {
try { try {
await this.rm(); await this.rm();
} catch (e) {} } catch (e) {}
dockerArgs = `--name ${this.name} -p 3306:${this.dbConf.port}`; dockerArgs = ['--name', this.name, '-p', `3306:${this.dbConf.port}`];
} }
let runChown = process.platform != 'linux'; let runChown = process.platform != 'linux';
const container = await this.execFile('docker', [
const container = await this.execP(`docker run --env RUN_CHOWN=${runChown} -d ${dockerArgs} ${this.serverImage}`); 'run',
'--env', `RUN_CHOWN=${runChown}`,
'-d',
...dockerArgs,
this.imageTag
]);
this.id = container.stdout.trim(); this.id = container.stdout.trim();
try { try {
if (this.isRandom) { if (this.isRandom) {
let inspect = await this.execP(`docker inspect -f "{{json .NetworkSettings}}" ${this.id}`); let netSettings = await this.execJson('docker', [
let netSettings = JSON.parse(inspect.stdout); 'inspect', '-f', '{{json .NetworkSettings}}', this.id
]);
if (ci) if (ci)
this.dbConf.host = netSettings.Gateway; this.dbConf.host = netSettings.Gateway;
@ -83,8 +100,9 @@ module.exports = class Docker {
async start() { async start() {
let state; let state;
try { try {
let result = await this.execP(`docker inspect -f "{{json .State}}" ${this.id}`); state = await this.execJson('docker', [
state = JSON.parse(result.stdout); 'inspect', '-f', '{{json .State}}', this.id
]);
} catch (err) { } catch (err) {
return await this.run(); return await this.run();
} }
@ -93,7 +111,7 @@ module.exports = class Docker {
case 'running': case 'running':
return; return;
case 'exited': case 'exited':
await this.execP(`docker start ${this.id}`); await this.execFile('docker', ['start', this.id]);
await this.wait(); await this.wait();
return; return;
default: default:
@ -107,15 +125,17 @@ module.exports = class Docker {
let elapsedTime = 0; let elapsedTime = 0;
let maxInterval = 4 * 60 * 1000; let maxInterval = 4 * 60 * 1000;
log('Waiting for MySQL init process...'); log('Waiting for container to be ready...');
async function checker() { async function checker() {
elapsedTime += interval; elapsedTime += interval;
let status; let status;
try { try {
let result = await this.execP(`docker inspect -f "{{.State.Health.Status}}" ${this.id}`); let status = await this.execJson('docker', [
status = result.stdout.trimEnd(); 'inspect', '-f', '{{.State.Health.Status}}', this.id
]);
status = status.trimEnd();
} catch (err) { } catch (err) {
return reject(new Error(err.message)); return reject(new Error(err.message));
} }
@ -124,12 +144,12 @@ module.exports = class Docker {
return reject(new Error('Docker exited, please see the docker logs for more info')); return reject(new Error('Docker exited, please see the docker logs for more info'));
if (status == 'healthy') { if (status == 'healthy') {
log('MySQL process ready.'); log('Container ready.');
return resolve(); return resolve();
} }
if (elapsedTime >= maxInterval) if (elapsedTime >= maxInterval)
reject(new Error(`MySQL not initialized whithin ${elapsedTime / 1000} secs`)); reject(new Error(`Container initialized whithin ${elapsedTime / 1000} secs`));
else else
setTimeout(bindedChecker, interval); setTimeout(bindedChecker, interval);
} }
@ -160,8 +180,9 @@ module.exports = class Docker {
let state; let state;
try { try {
let result = await this.execP(`docker inspect -f "{{json .State}}" ${this.id}`); state = await this.execJson('docker', [
state = JSON.parse(result.stdout); 'inspect', '-f', '{{json .State}}', this.id
]);
} catch (err) { } catch (err) {
return reject(new Error(err.message)); return reject(new Error(err.message));
} }
@ -189,19 +210,23 @@ module.exports = class Docker {
}); });
} }
rm() { async rm() {
return this.execP(`docker stop ${this.id} && docker rm -v ${this.id}`); try {
await this.execFile('docker', ['stop', this.id]);
await this.execFile('docker', ['rm', '-v', this.id]);
} catch (e) {}
} }
/** /**
* Promisified version of exec(). * Promisified version of execFile().
* *
* @param {String} command The exec command * @param {String} command The exec command
* @param {Array} args The command arguments
* @return {Promise} The promise * @return {Promise} The promise
*/ */
execP(command) { execFile(command, args) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec(command, (err, stdout, stderr) => { execFile(command, args, (err, stdout, stderr) => {
if (err) if (err)
reject(err); reject(err);
else { else {
@ -213,4 +238,16 @@ module.exports = class Docker {
}); });
}); });
} }
/**
* 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);
}
}; };

View File

@ -1,4 +1,4 @@
#!/usr/bin/node
const fs = require('fs-extra'); const fs = require('fs-extra');
const mysql = require('mysql2/promise'); const mysql = require('mysql2/promise');
const ejs = require('ejs'); const ejs = require('ejs');
@ -48,9 +48,7 @@ const exporters = [
// Exports objects for all schemas // Exports objects for all schemas
module.exports = async function main(opts, config, dbConf) { module.exports = async function main(workdir, schemas, dbConf) {
const exportDir = `${opts.workdir}/routines`;
const conn = await mysql.createConnection(dbConf); const conn = await mysql.createConnection(dbConf);
conn.queryFromFile = function(file, params) { conn.queryFromFile = function(file, params) {
return this.execute( return this.execute(
@ -60,12 +58,12 @@ module.exports = async function main(opts, config, dbConf) {
} }
try { try {
const exportDir = `${workdir}/routines`;
if (fs.existsSync(exportDir)) if (fs.existsSync(exportDir))
fs.removeSync(exportDir, {recursive: true}); fs.removeSync(exportDir, {recursive: true});
fs.mkdirSync(exportDir); fs.mkdirSync(exportDir);
for (let schema of config.structure) { for (let schema of schemas) {
let schemaDir = `${exportDir}/${schema}`; let schemaDir = `${exportDir}/${schema}`;
if (!fs.existsSync(schemaDir)) if (!fs.existsSync(schemaDir))

View File

@ -5,7 +5,8 @@ CONFIG_FILE=$1
INI_FILE=$2 INI_FILE=$2
DUMP_FILE="dump/structure.sql" DUMP_FILE="dump/structure.sql"
SCHEMAS=( $(jq -r ".structure[]" "$CONFIG_FILE") ) echo "SELECT 1;" | mysql --defaults-file="$INI_FILE" >> /dev/null
SCHEMAS=( $(jq -r ".schemas[]" "$CONFIG_FILE") )
mysqldump \ mysqldump \
--defaults-file="$INI_FILE" \ --defaults-file="$INI_FILE" \

210
index.js
View File

@ -2,15 +2,15 @@
require('colors'); require('colors');
const getopts = require('getopts'); const getopts = require('getopts');
const package = require('./package.json'); const package = require('./package.json');
const dockerRun = require('./docker-run');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path');
const ini = require('ini'); const ini = require('ini');
const path = require('path');
const dockerRun = require('./docker-run');
console.log('MyVC (MySQL Version Control)'.green, `v${package.version}`.magenta); console.log('MyVC (MySQL Version Control)'.green, `v${package.version}`.magenta);
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
const opts = getopts(argv, { const cliOpts = getopts(argv, {
alias: { alias: {
env: 'e', env: 'e',
workdir: 'w', workdir: 'w',
@ -23,23 +23,14 @@ const opts = getopts(argv, {
} }
}) })
if (opts.version) if (cliOpts.version)
process.exit(0); process.exit(0);
function usage() { const action = cliOpts._[0];
console.log('Usage:'.gray, 'myvc [-w|--workdir] [-e|--env] [-h|--help] action'.magenta); if (!action) {
console.log('Usage:'.gray, '[npx] myvc [-w|--workdir] [-e|--env] [-h|--help] action'.blue);
process.exit(0); process.exit(0);
} }
function error(message) {
console.error('Error:'.gray, message.red);
process.exit(1);
}
function parameter(parameter, value) {
console.log(parameter.gray, value.blue);
}
const action = opts._[0];
if (!action) usage();
const actionArgs = { const actionArgs = {
apply: { apply: {
@ -55,80 +46,139 @@ const actionArgs = {
} }
}; };
const actionOpts = getopts(argv, actionArgs[action]); const actionOpts = getopts(argv, actionArgs[action]);
Object.assign(opts, actionOpts); Object.assign(cliOpts, actionOpts);
const opts = {};
for (let opt in cliOpts) {
if (opt.length > 1 || opt == '_')
opts[opt] = cliOpts[opt];
}
function parameter(parameter, value) {
console.log(parameter.gray, value.blue);
}
parameter('Environment:', opts.env); parameter('Environment:', opts.env);
parameter('Workdir:', opts.workdir); parameter('Workdir:', opts.workdir);
parameter('Action:', action); parameter('Action:', action);
// Configuration file class MyVC {
async init(opts) {
// Configuration file
const configFile = 'myvc.config.json';
const configPath = path.join(opts.workdir, configFile);
if (!await fs.pathExists(configPath))
throw new Error(`Config file not found: ${configFile}`);
const config = require(configPath);
const configFile = 'myvc.config.json'; Object.assign(opts, config);
const configPath = path.join(opts.workdir, configFile); opts.configFile = configFile;
if (!fs.existsSync(configPath))
error(`Config file not found: ${configFile}`); // Database configuration
const config = require(configPath);
let iniFile = 'db.ini';
// Database configuration let iniDir = __dirname;
if (opts.env) {
let iniFile = 'db.ini'; iniFile = `db.${opts.env}.ini`;
let iniDir = __dirname; iniDir = opts.workdir;
if (opts.env) { }
iniFile = `db.${opts.env}.ini`; const iniPath = path.join(iniDir, iniFile);
iniDir = opts.workdir;
} if (!await fs.pathExists(iniPath))
const iniPath = path.join(iniDir, iniFile); throw new Error(`Database config file not found: ${iniFile}`);
if (!fs.existsSync(iniPath)) const iniConfig = ini.parse(await fs.readFile(iniPath, 'utf8')).client;
error(`Database config file not found: ${iniFile}`); const dbConfig = {
host: !opts.env ? 'localhost' : iniConfig.host,
const iniConfig = ini.parse(fs.readFileSync(iniPath, 'utf8')).client; port: iniConfig.port,
const dbConfig = { user: iniConfig.user,
host: !opts.env ? 'localhost' : iniConfig.host, password: iniConfig.password,
port: iniConfig.port, authPlugins: {
user: iniConfig.user, mysql_clear_password() {
password: iniConfig.password, return () => iniConfig.password + '\0';
authPlugins: { }
mysql_clear_password() { }
return () => iniConfig.password + '\0'; };
if (iniConfig.ssl_ca) {
dbConfig.ssl = {
ca: await fs.readFile(`${opts.workdir}/${iniConfig.ssl_ca}`),
rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined
}
} }
}
};
if (iniConfig.ssl_ca) { Object.assign(opts, {
dbConfig.ssl = { iniFile,
ca: fs.readFileSync(`${opts.workdir}/${iniConfig.ssl_ca}`), dbConfig
rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined });
}
async structure (opts) {
await dockerRun('export-structure.sh',
opts.workdir,
opts.configFile,
opts.iniFile
);
}
async fixtures(opts) {
await dockerRun('export-fixtures.sh',
opts.workdir,
opts.configFile,
opts.iniFile
);
}
async routines(opts) {
const exportRoutines = require('./export-routines');
await exportRoutines(
opts.workdir,
opts.schemas,
opts.dbConfig
);
}
async apply(opts) {
let args = [];
if (opts.force) args.push('-f');
if (opts.user) args.push('-u');
if (opts.env) args = args.concat(['-e', opts.env]);
await dockerRun('apply-changes.sh',
opts.workdir,
...args
);
}
async run(opts) {
const Docker = require('./docker');
const container = new Docker(opts.code, opts.workdir);
await container.run();
}
async start(opts) {
const Docker = require('./docker');
const container = new Docker(opts.code, opts.workdir);
await container.start();
} }
} }
// Actions (async function() {
try {
const myvc = new MyVC();
switch (action) { if (myvc[action]) {
case 'structure': await myvc.init(opts);
dockerRun('export-structure.sh', opts.workdir, configFile, iniFile); await myvc[action](opts);
break; } else
case 'fixtures': throw new Error (`Unknown action '${action}'`);
dockerRun('export-fixtures.sh', opts.workdir, configFile, iniFile); } catch (err) {
break; if (err.name == 'Error')
case 'routines': console.error('Error:'.gray, err.message.red);
require('./export-routines')(opts, config, dbConfig); else
break; throw err;
case 'apply':
dockerRun('apply-changes.sh', opts.workdir, ...argv);
break;
case 'run': {
const Docker = require('./docker');
const container = new Docker(config.code, opts.workdir);
container.run();
break;
} }
case 'start': { })();
const Docker = require('./docker');
const container = new Docker(config.code, opts.workdir); module.exports = MyVC;
container.start();
break;
}
default:
usage();
}

View File

@ -1,17 +1,17 @@
{ {
"code": "my-app", "code": "my-app",
"structure": [ "schemas": [
"schema1", "util",
"schema2" "my_app"
], ],
"fixtures": { "fixtures": {
"schema1": [ "util": [
"table1.1", "version",
"table2.1" "versionUser"
], ],
"schema2": [ "my_app": [
"table2.1", "table1",
"table2.2" "table2"
] ]
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "myvc", "name": "myvc",
"version": "1.0.6", "version": "1.0.7",
"author": "Verdnatura Levante SL", "author": "Verdnatura Levante SL",
"description": "MySQL Version Control", "description": "MySQL Version Control",
"license": "GPL-3.0", "license": "GPL-3.0",
@ -28,7 +28,11 @@
"keywords": [ "keywords": [
"mysql", "mysql",
"mariadb", "mariadb",
"git",
"vcs",
"database",
"version", "version",
"control" "control",
"sql"
] ]
} }