Refactor and error fixes

This commit is contained in:
Juan Ferrer 2020-12-04 10:15:29 +01:00
parent 9d71ee7d23
commit 598ecbc535
11 changed files with 181 additions and 199 deletions

View File

@ -79,7 +79,7 @@ const docker = {
child.on('exit', code => {
if (code !== 0) {
const args = JSON.stringify(execArgs);
reject(new Error(`'docker' ${args}: Exit code: ${code}`));
reject(new Error(`docker: ${args}: exit code ${code}`));
} else
resolve(code);
});
@ -96,7 +96,9 @@ const docker = {
};
class Container {
construct(id) {
constructor(id) {
if (!id)
throw new Error('Container id argument is required');
this.id = id;
}
@ -122,4 +124,5 @@ function camelToSnake(str) {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
}
module.exports = docker;
module.exports = docker;
module.exports.Container = Container;

View File

@ -21,6 +21,7 @@ class MyVC {
alias: {
env: 'e',
workspace: 'w',
socket: 's',
debug: 'd',
version: 'v',
help: 'h'
@ -141,7 +142,7 @@ class MyVC {
rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined
}
}
if (!opts.env)
if (opts.socket)
dbConfig.socketPath = '/var/run/mysqld/mysqld.sock';
Object.assign(opts, {

View File

@ -2,8 +2,11 @@
const MyVC = require('./index');
const fs = require('fs-extra');
const path = require('path');
const docker = require('./docker');
const docker = require('./docker').docker;
/**
* Dumps structure and fixtures from remote.
*/
class Dump {
get myOpts() {
return {
@ -36,7 +39,7 @@ class Dump {
await docker.build(__dirname, {
tag: 'myvc/client',
file: path.join(__dirname, 'Dockerfile.client')
}, !!this.opts.debug);
}, opts.debug);
let dumpArgs = [
`--defaults-file=${opts.iniFile}`,

View File

@ -1,94 +1,20 @@
const MyVC = require('./index');
const fs = require('fs-extra');
const path = require('path');
const typeMap = {
events: {
name: 'EVENT',
abbr: 'EVNT',
color: 'cyan'
},
functions: {
name: 'FUNCTION',
abbr: 'FUNC',
color: 'cyan'
},
procedures: {
name: 'PROCEDURE',
abbr: 'PROC',
color: 'yellow'
},
triggers: {
name: 'TRIGGER',
abbr: 'TRIG',
color: 'blue'
},
views: {
name: 'VIEW',
abbr: 'VIEW',
color: 'magenta'
},
};
class Routine {
construct(path, mark) {
const path = path
const split = path.split('/');
const fullPath = `${this.opts.workspace}/routines/${path}.sql`;
const schema = split[0];
const type = typeMap[split[1]];
const name = split[2];
Object.assign(this, {
path,
mark: mark,
exists: await fs.pathExists(fullPath),
type,
schema,
name,
fullName: `${schema}.${name}`,
isRoutine: ['FUNC', 'PROC'].indexOf(type.abbr) !== -1
});
}
}
const tokens = {
string: {
start: '\'',
end: '\'',
escape: char => char == '\'' || char == '\\'
},
id: {
start: '`',
end: '`',
escape: char => char == '`'
},
multiComment: {
start: '/*',
end: '*/',
escape: () => false
},
singleComment: {
start: '-- ',
end: '\n',
escape: () => false
}
};
const tokenIndex = new Map();
for (const tokenId in tokens) {
const token = tokens[tokenId];
tokenIndex.set(token.start[0], token);
}
/**
* Pushes changes to remote.
*
* @property {Boolean} force Answer yes to all questions
* @property {Boolean} user Whether to change current user version
*/
class Push {
get myOpts() {
return {
alias: {
force: 'f',
user: 'u',
applyUncommited: 'a'
user: 'u'
}
};
}
@ -218,7 +144,7 @@ class Push {
let changes = await fs.pathExists(`${opts.workspace}/.git`)
? await myvc.changedRoutines(version.gitCommit)
: await myvc.cachedChanges();
changes = await this.parseChanges(changes);
changes = this.parseChanges(changes);
await conn.query(
`CREATE TEMPORARY TABLE tProcsPriv
@ -245,12 +171,14 @@ class Push {
}
for (const change of changes) {
const actionMsg = change.exists ? '[+]'.green : '[-]'.red;
const fullPath = `${opts.workspace}/routines/${change.path}.sql`;
const exists = await fs.pathExists(fullPath);
const actionMsg = exists ? '[+]'.green : '[-]'.red;
const typeMsg = `[${change.type.abbr}]`[change.type.color];
console.log('', actionMsg.bold, typeMsg.bold, change.fullName);
if (change.exists)
if (exists)
await this.queryFromFile(pushConn, `routines/${change.path}.sql`);
else {
const escapedName =
@ -285,7 +213,7 @@ class Push {
console.log(` -> No routines changed.`);
}
async parseChanges(changes) {
parseChanges(changes) {
const routines = [];
for (const change of changes)
routines.push(new Routine(change));
@ -413,6 +341,84 @@ class Push {
}
}
const typeMap = {
events: {
name: 'EVENT',
abbr: 'EVNT',
color: 'cyan'
},
functions: {
name: 'FUNCTION',
abbr: 'FUNC',
color: 'cyan'
},
procedures: {
name: 'PROCEDURE',
abbr: 'PROC',
color: 'yellow'
},
triggers: {
name: 'TRIGGER',
abbr: 'TRIG',
color: 'blue'
},
views: {
name: 'VIEW',
abbr: 'VIEW',
color: 'magenta'
},
};
class Routine {
constructor(change) {
const path = change.path;
const split = path.split('/');
const schema = split[0];
const type = typeMap[split[1]];
const name = split[2];
Object.assign(this, {
path,
mark: change.mark,
type,
schema,
name,
fullName: `${schema}.${name}`,
isRoutine: ['FUNC', 'PROC'].indexOf(type.abbr) !== -1
});
}
}
const tokens = {
string: {
start: '\'',
end: '\'',
escape: char => char == '\'' || char == '\\'
},
id: {
start: '`',
end: '`',
escape: char => char == '`'
},
multiComment: {
start: '/*',
end: '*/',
escape: () => false
},
singleComment: {
start: '-- ',
end: '\n',
escape: () => false
}
};
const tokenIndex = new Map();
for (const tokenId in tokens) {
const token = tokens[tokenId];
tokenIndex.set(token.start[0], token);
}
module.exports = Push;
if (require.main === module)

View File

@ -2,6 +2,7 @@
const MyVC = require('./index');
const docker = require('./docker');
const fs = require('fs-extra');
const path = require('path');
const Server = require('./server/server');
/**
@ -10,7 +11,8 @@ const Server = require('./server/server');
* 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
* @property {Boolean} ci Continuous integration environment
* @property {Boolean} random Whether to use a random container name
*/
class Run {
get myOpts() {
@ -23,32 +25,39 @@ class Run {
}
async run(myvc, opts) {
const server = new Server(opts.code, opts.workspace);
await server.run();
const dumpDir = `${opts.workspace}/dump`;
const dumpInfo = `${dumpDir}/.dump.json`;
if (await fs.pathExists(dumpInfo)) {
const cache = await myvc.cachedChanges();
const version = JSON.parse(
await fs.readFileSync(dumpInfo, 'utf8')
);
const fd = await fs.open(`${dumpDir}/.changes`, 'w+');
const changes = await myvc.changedRoutines(version.gitCommit);
for (const change of changes)
fs.write(fd, change.mark + change.path + '\n');
let isEqual = false;
if (cache && changes && cache.length == changes.lenth)
for (let i = 0; i < changes.length; i++) {
isEqual = cache[i].path == changes[i].path
&& cache[i].mark == changes[i].mark;
if (!isEqual) break;
}
await fs.close(fd);
if (!isEqual) {
const fd = await fs.open(`${dumpDir}/.changes`, 'w+');
for (const change of changes)
fs.write(fd, change.mark + change.path + '\n');
await fs.close(fd);
}
}
const dockerfilePath = path.join(__dirname, 'server', 'Dockerfile');
await docker.build(__dirname, {
tag: 'myvc/server',
file: `${dockerfilePath}.server`
});
file: dockerfilePath
}, opts.debug);
const today = new Date();
const pad = v => v < 10 ? '0' + v : v;
@ -57,20 +66,23 @@ class Run {
const day = pad(today.getDate());
const stamp = `${year}-${month}-${day}`;
await docker.build(__dirname, {
tag: this.imageTag,
await docker.build(opts.workspace, {
tag: opts.code,
file: `${dockerfilePath}.dump`,
buildArg: `STAMP=${stamp}`
});
}, opts.debug);
const isRandom = opts.random;
const dbConfig = Object.assign({}, opts.dbConfig);
let runOptions;
if (this.isRandom)
if (isRandom)
runOptions = {publish: '3306'};
else {
runOptions = {
name: this.name,
publish: `3306:${this.dbConf.port}`
name: opts.code,
publish: `3306:${dbConfig.port}`
};
try {
await this.rm();
@ -83,26 +95,28 @@ class Run {
env: `RUN_CHOWN=${runChown}`,
detach: true
});
const ct = await docker.run(this.imageTag, null, runOptions);
const ct = await docker.run(opts.code, null, runOptions);
const server = new Server(ct, dbConfig);
try {
if (this.isRandom) {
if (isRandom) {
const netSettings = await ct.inspect({
filter: '{{json .NetworkSettings}}'
format: '{{json .NetworkSettings}}'
});
if (opts.ci)
this.dbConf.host = netSettings.Gateway;
dbConfig.host = netSettings.Gateway;
this.dbConf.port = netSettings.Ports['3306/tcp'][0]['HostPort'];
dbConfig.port = netSettings.Ports['3306/tcp'][0].HostPort;
}
await this.wait();
} catch (err) {
if (this.isRandom)
await this.rm();
if (isRandom)
await server.rm();
throw err;
}
await server.wait();
return server;
}
}

View File

@ -1,7 +1,8 @@
const MyVC = require('./index');
const docker = require('./docker');
const Container = require('./docker').Container;
const Server = require('./server/server');
const Run = require('./myvc-run');
/**
* Does the minium effort to start the database container, if it doesn't
@ -11,28 +12,31 @@ const Server = require('./server/server');
*/
class Start {
async run(myvc, opts) {
const server = new Server(opts.code, opts.workspace);
await server.start();
const ct = new Container(opts.code);
let status;
try {
status = await docker.inspect(opts.code, {
filter: '{{json .State.Status}}'
status = await ct.inspect({
format: '{{json .State.Status}}'
});
} catch (err) {
return await this.run();
const run = new Run()
return await run.run(myvc, opts);
}
switch (status) {
case 'running':
return;
break;
case 'exited':
await docker.start(opts.code);
await this.wait();
return;
await ct.start();
break;
default:
throw new Error(`Unknown docker status: ${status}`);
}
const server = new Server(ct, opts.dbConfig);
await server.wait();
return server;
}
}

41
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "myvc",
"version": "1.0.18",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -75,14 +75,6 @@
"uri-js": "^4.2.2"
}
},
"ansi-gray": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz",
"integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=",
"requires": {
"ansi-wrap": "0.1.0"
}
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@ -96,11 +88,6 @@
"color-convert": "^1.9.0"
}
},
"ansi-wrap": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz",
"integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768="
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@ -282,11 +269,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@ -419,17 +401,6 @@
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"fancy-log": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz",
"integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==",
"requires": {
"ansi-gray": "^0.1.1",
"color-support": "^1.1.3",
"parse-node-version": "^1.0.0",
"time-stamp": "^1.0.0"
}
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -1106,11 +1077,6 @@
"p-finally": "^1.0.0"
}
},
"parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -1393,11 +1359,6 @@
"xtend": "^4.0.0"
}
},
"time-stamp": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM="
},
"to-buffer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "myvc",
"version": "1.1.0",
"version": "1.1.1",
"author": "Verdnatura Levante SL",
"description": "MySQL Version Control",
"license": "GPL-3.0",
@ -13,7 +13,6 @@
"@sqltools/formatter": "^1.2.2",
"colors": "^1.4.0",
"ejs": "^3.1.5",
"fancy-log": "^1.3.3",
"fs-extra": "^8.1.0",
"getopts": "^2.2.5",
"ini": "^1.3.5",

View File

@ -34,6 +34,7 @@ COPY \
structure.sql \
index.js \
myvc.js \
myvc-push.js \
myvc.default.yml \
db.ini \
./

View File

@ -23,7 +23,7 @@ COPY \
ARG STAMP=unknown
RUN gosu mysql docker-temp-start.sh \
&& myvc push \
&& myvc push --socket \
&& docker-dump.sh dump/fixtures \
&& gosu mysql docker-temp-stop.sh

View File

@ -1,22 +1,11 @@
const log = require('fancy-log');
const path = require('path');
const docker = require('../docker');
const mysql = require('mysql2/promise');
module.exports = class Server {
constructor(name, context) {
constructor(ct, dbConfig) {
Object.assign(this, {
id: name,
name,
isRandom: name == null,
dbConf: {
host: 'localhost',
port: '3306',
username: 'root',
password: 'root'
},
imageTag: name || 'myvc/dump',
context
ct,
dbConfig
});
}
@ -28,22 +17,23 @@ module.exports = class Server {
let elapsedTime = 0;
let maxInterval = 4 * 60 * 1000;
const dbConfig = this.dbConfig;
let myConf = {
user: this.dbConf.username,
password: this.dbConf.password,
host: this.dbConf.host,
port: this.dbConf.port
user: dbConfig.user,
password: dbConfig.password,
host: dbConfig.host,
port: dbConfig.port
};
log('Waiting for MySQL init process...');
console.log('Waiting for MySQL init process...');
async function checker() {
elapsedTime += interval;
let status;
try {
status = await docker.inspect(this.id, {
filter: '{{json .State.Status}}'
status = await this.ct.inspect({
format: '{{json .State.Status}}'
});
} catch (err) {
return reject(new Error(err.message));
@ -52,12 +42,12 @@ module.exports = class Server {
if (status === 'exited')
return reject(new Error('Docker exited, please see the docker logs for more info'));
let conn = mysql.createConnection(myConf);
const conn = mysql.createConnection(myConf);
conn.on('error', () => {});
conn.connect(err => {
conn.destroy();
if (!err) {
log('MySQL process ready.');
console.log('MySQL process ready.');
return resolve();
}
@ -67,15 +57,15 @@ module.exports = class Server {
setTimeout(bindedChecker, interval);
});
}
let bindedChecker = checker.bind(this);
const bindedChecker = checker.bind(this);
bindedChecker();
});
}
async rm() {
try {
await docker.stop(this.id);
await docker.rm(this.id, {volumes: true});
await this.ct.stop();
await this.ct.rm({volumes: true});
} catch (e) {}
}
};