diff --git a/docker.js b/docker.js index 77d57ff..d84665d 100644 --- a/docker.js +++ b/docker.js @@ -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; \ No newline at end of file +module.exports = docker; +module.exports.Container = Container; diff --git a/index.js b/index.js index 93611aa..3e337c5 100644 --- a/index.js +++ b/index.js @@ -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, { diff --git a/myvc-dump.js b/myvc-dump.js index 0a9bbef..fd2285e 100644 --- a/myvc-dump.js +++ b/myvc-dump.js @@ -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}`, diff --git a/myvc-push.js b/myvc-push.js index 43feed2..5982e29 100644 --- a/myvc-push.js +++ b/myvc-push.js @@ -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) diff --git a/myvc-run.js b/myvc-run.js index 96d0d10..fa3cee1 100644 --- a/myvc-run.js +++ b/myvc-run.js @@ -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; } } diff --git a/myvc-start.js b/myvc-start.js index 287ad7b..e590f27 100644 --- a/myvc-start.js +++ b/myvc-start.js @@ -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; } } diff --git a/package-lock.json b/package-lock.json index 22a14cb..29e8ce8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f5f89ae..eef95fc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/Dockerfile b/server/Dockerfile index 9b8eb6c..4b38305 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -34,6 +34,7 @@ COPY \ structure.sql \ index.js \ myvc.js \ + myvc-push.js \ myvc.default.yml \ db.ini \ ./ diff --git a/server/Dockerfile.dump b/server/Dockerfile.dump index a93cc55..57d0806 100644 --- a/server/Dockerfile.dump +++ b/server/Dockerfile.dump @@ -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 diff --git a/server/server.js b/server/server.js index 72868be..6ffbd77 100644 --- a/server/server.js +++ b/server/server.js @@ -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) {} } };