diff --git a/.jshintrc b/.jshintrc index 6b4c1a9..f57a8ff 100644 --- a/.jshintrc +++ b/.jshintrc @@ -9,6 +9,5 @@ "undef": true, "boss": true, "eqnull": true, - "node": true, - "es5": true + "node": true } diff --git a/Gruntfile.js b/Gruntfile.js index e364a61..76fcdda 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,7 +2,7 @@ * grunt-deployments * https://github.com/getdave/grunt-deployments * - * Copyright (c) 2013 David Smith + * Copyright (c) 2014 David Smith * Licensed under the MIT license. */ @@ -10,56 +10,98 @@ module.exports = function(grunt) { + // load all grunt tasks + require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); // Project configuration. grunt.initConfig({ - db_fixture: grunt.file.readJSON('test/fixtures/test_db.json'), - + db_fixture: grunt.file.readJSON('test/fixtures/basic_config.json'), + vows: { + all: { + options: { + // String {spec|json|dot-matrix|xunit|tap} + // defaults to "dot-matrix" + reporter: "spec", + // String or RegExp which is + // matched against title to + // restrict which tests to run + // onlyRun: /helper/, + // Boolean, defaults to false + verbose: false, + // Boolean, defaults to false + silent: false, + // Colorize reporter output, + // boolean, defaults to true + colors: true, + // Run each test in its own + // vows process, defaults to + // false + isolate: false, + // String {plain|html|json|xml} + // defaults to none + coverage: "json" + }, + // String or array of strings + // determining which files to include. + // This option is grunt's "full" file format. + src: ["test/**/*_test.js"] + } + }, jshint: { - all: [ - 'Gruntfile.js', - 'tasks/*.js', - '<%= nodeunit.tests %>', - ], + gruntfile: { + src: 'Gruntfile.js' + }, + lib: { + src: ['tasks/*.js'] + }, + test: { + src: ['test/**/*.js'] + }, options: { jshintrc: '.jshintrc', }, }, + watch: { + gruntfile: { + files: '<%= jshint.gruntfile.src %>', + tasks: ['jshint:gruntfile'] + }, + lib: { + files: '<%= jshint.lib.src %>', + tasks: ['jshint:lib', 'vows'] + }, + test: { + files: '<%= jshint.test.src %>', + tasks: ['jshint:test', 'vows'] + } + }, + // Before generating any new files, remove any previously-created files. clean: { tests: ['tmp'], + backups: ['./backups'], }, deployments: { options: { backups_dir: '' }, - local: '<%= db_fixture.local %>', - develop: '<%= db_fixture.develop %>' - }, - - // Unit tests. - nodeunit: { - tests: ['test/*_test.js'], + local: '<%= db_fixture.local %>', // make sure you've created valid fixture DB creds + develop: '<%= db_fixture.develop %>' // make sure you've created valid fixture DB creds }, }); - // Actually load this plugin's task(s). +// Actually load this plugin's task(s). grunt.loadTasks('tasks'); - // These plugins provide necessary tasks. - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-clean'); - grunt.loadNpmTasks('grunt-contrib-nodeunit'); - // Whenever the "test" task is run, first clean the "tmp" dir, then run this // plugin's task(s), then test the result. - grunt.registerTask('test', ['clean', 'deployments', 'nodeunit']); + grunt.registerTask('test', ['clean', 'vows']); // By default, lint and run all tests. - grunt.registerTask('default', ['jshint', 'test']); + grunt.registerTask('default', ['clean', 'watch']); }; diff --git a/README.md b/README.md index 5bf4625..53527c4 100644 --- a/README.md +++ b/README.md @@ -43,50 +43,34 @@ grunt.initConfig({ }); ``` -**IMPORTANT NOTE:** The task is opinionated in that it assumes you are working on a local machine and pushing/pulling databases from/to that location. Thus it is imperative that you define a `local` target as part of your configuration. +**UPDATE:** The task was originally opinionated in that it once assumed you were working on a local machine and pushing/pulling databases from/to that location. Therefore it *was* imperative that you defined a `local` target as part of your configuration. The task has now been updated to allow you to avoid having to utilise a "local" target. We still advise however, that you define a "local" target as a fallback. +Please ensure you read the full Configuration documentation below before proceeding. -### Available Tasks - -The Plugin makes two new tasks available via Grunt. These are `db_pull` and `db_push`. The interface for both commands is identical: - -````grunt db_pull --target="%%TARGET%%" // replace %%TARGET%% with the target you've defined in your config ```` - -There is a single argument `--target` that is required each time you run either command. - -#### Task: db_push - -The `db_push` command moves your **local** database to a **remote** database location. The following process is observed: - -1. Takes a dump of your local database -2. Runs a search and replace on the local dump file -3. Backups up the target database (remote) -4. Imports the local dump into the target database -The `target` argument represents the remote target to which you wish to push your database. +### Available Tasks -````grunt db_push --target="develop"```` +The Plugin makes use of a **single** task `db_pull`. The interface for this command is as follows: -#### Task: db_pull +````grunt db_pull```` -The `db_pull` command pulls a **remote** database into your **local** environment. The following process is observed: +There are two flags that should be used each time you run either command: -1. Takes a dump of the remote database -2. Runs a search and replace on the dump file -3. Backups up your local database -4. Imports the remote dump into your local database +* `--src` - the source database from which you wish to export your SQL +* `--dest` - the destination database into which you would like to import your SQL -The `target` argument represents the remote target whose database you wish to pull into your local environment. Eg: +#### Example -````grunt db_pull --target="stage"```` +````grunt db_pull --src="%%SOURCE_DB%%" --dest="%%DESTINATION_DB%%"```` +__Note:__ only `src` is required. If `dest` is not provided the task will automatically assume you wish to use the `local` target defined in your Grunt task configuration. This is for ease of use and also to maintain backwards compatibility with the older CLI. -### Usage +### Configuration -#### Local Target (required) -As above, the Plugin task is opinionated. It *expects* that you are working locally and pushing/pulling from/to that location. +#### Local Target (recommended ~~required~~) +Whilst the Plugin task (no longer) forces you to define a `local` target, we still advise that you always define one. This is because the `local` target will be used as the default destination if one is not explicity provided. -As a result, it is essential that you define a *single* target *without* an `ssh_host` parameter. This is typically named "local" for convenience. +Your local target should not require a `ssh_host` parameter and, to avoid complication, should be named exactly as `"local"`. ```js "local": { @@ -95,15 +79,15 @@ As a result, it is essential that you define a *single* target *without* an `ssh "user": "local_db_username", "pass": "local_db_password", "host": "local_db_host", - "url": "local_db_url" + "url": "local_db_url", // note that the `local` target does not have an "ssh_host" }, ``` -The task will assume that this target is equivilant to your `local` environment. You can call it anything you wish but it ***must not*** have an `ssh_host` parameter. +Again, the "local" target ***must not*** have an `ssh_host` parameter. #### Other Environment Targets -All other targets *must* contain a valid `ssh_host` parameter. +All other targets may contain valid ssh credentials. ```js "develop": { @@ -113,7 +97,10 @@ All other targets *must* contain a valid `ssh_host` parameter. "pass": "development_db_password", "host": "development_db_host", "url": "development_db_url", - "ssh_host": "ssh_user@ssh_host" + "ssh_user": "ssh_user", // UPDATE: user/host now defined separately + "ssh_host": "ssh_host", // UPDATE: user/host now defined separately + "ssh_port": "ssh_port", + "ignoreTables": ["table1","table2",...] }, "stage": { "title": "Stage", @@ -122,7 +109,10 @@ All other targets *must* contain a valid `ssh_host` parameter. "pass": "stage_db_password", "host": "stage_db_host", "url": "stage_db_url", - "ssh_host": "ssh_user@ssh_host" + "ssh_user": "ssh_user", // UPDATE: user/host now defined separately + "ssh_host": "ssh_host", // UPDATE: user/host now defined separately + "ssh_port": "ssh_port", + "ignoreTables": ["table1","table2",...] }, "production": { "title": "Production", @@ -131,7 +121,10 @@ All other targets *must* contain a valid `ssh_host` parameter. "pass": "production_db_password", "host": "production_db_host", "url": "production_db_url", - "ssh_host": "ssh_user@ssh_host" + "ssh_user": "ssh_user", // UPDATE: user/host now defined separately + "ssh_host": "ssh_host", // UPDATE: user/host now defined separately + "ssh_port": "ssh_port", + "ignoreTables": ["table1","table2",...] } ``` @@ -152,7 +145,8 @@ grunt.initConfig({ "user": "local_db_username", "pass": "local_db_password", "host": "local_db_host", - "url": "local_db_url" + "url": "local_db_url", + "ignoreTables": ["table1","table2",...] // note that the `local` target does not have an "ssh_host" }, // "Remote" targets @@ -163,7 +157,10 @@ grunt.initConfig({ "pass": "development_db_password", "host": "development_db_host", "url": "development_db_url", - "ssh_host": "ssh_user@ssh_host" + "ssh_user": "ssh_user", // UPDATE: user/host now defined separately + "ssh_host": "ssh_host", // UPDATE: user/host now defined separately + "ssh_port": "ssh_port", + "ignoreTables": ["table1","table2",...] }, "stage": { "title": "Stage", @@ -172,7 +169,10 @@ grunt.initConfig({ "pass": "stage_db_password", "host": "stage_db_host", "url": "stage_db_url", - "ssh_host": "ssh_user@ssh_host" + "ssh_user": "ssh_user", // UPDATE: user/host now defined separately + "ssh_host": "ssh_host", // UPDATE: user/host now defined separately + "ssh_port": "ssh_port", + "ignoreTables": ["table1","table2",...] }, "production": { "title": "Production", @@ -181,7 +181,10 @@ grunt.initConfig({ "pass": "production_db_password", "host": "production_db_host", "url": "production_db_url", - "ssh_host": "ssh_user@ssh_host" + "ssh_user": "ssh_user", // UPDATE: user/host now defined separately + "ssh_host": "ssh_host", // UPDATE: user/host now defined separately + "ssh_port": "ssh_port", + "ignoreTables": ["table1","table2",...] } }, }) @@ -211,13 +214,31 @@ Description: the password for the database user (above) Type: `String` Description: the hostname for the location in which the database resides. Typically this will be `localhost` +#### port +Type: `Integer` +Description: the port that MySQL is running on. Defaults to `3306` + #### url Type: `String` Description: the string to search and replace within the database before it is moved to the target location. Typically this is designed for use with systems such as WordPress where the `siteurl` value is [stored in the database](http://codex.wordpress.org/Changing_The_Site_URL) and is required to be updated upon migration to a new environment. It is however suitable for replacing any single value within the database before it is moved. +#### ssh_user +Type: `String` +Description: any valid ssh user. The task assumes you have ssh keys setup which allow you to remote into your server without requiring the input of a password. As this is an exhaustive topic we will not cover it here but you might like to start by reading [Github's own advice](https://help.github.com/articles/generating-ssh-keys). + #### ssh_host Type: `String` -Description: ssh connection string in the format `SSH_USER@SSH_HOST`. The task assumes you have ssh keys setup which allow you to remote into your server without requiring the input of a password. As this is an exhaustive topic we will not cover it here but you might like to start by reading [Github's own advice](https://help.github.com/articles/generating-ssh-keys). +Description: any valid ssh host string ~~in the format `SSH_USER@SSH_HOST`~~. The task assumes you have ssh keys setup which allow you to remote into your server without requiring the input of a password. As this is an exhaustive topic we will not cover it here but you might like to start by reading [Github's own advice](https://help.github.com/articles/generating-ssh-keys). + +#### ssh_port +Type: `String` +Default value: `22` +Description: SSH port number in the format `#####`. Defaults to the standard SSH port of `22`. + + +#### ignoreTables +Type: `Array` +Description: a list of tables to ignore in array format. Tables defined here will be ommitted from the dump. Neither their structure nor their content will be included. ### Options @@ -229,21 +250,103 @@ A string value that represents the directory path (*relative* to your Grunt file You may wish to have your backups reside outside the current working directory of your Gruntfile. In which case simply provide the relative path eg: ````../../backups````. -#### options.target +#### options.target (deprecated) -Type: `String` -Default value: `` +*The task now requires `src` and `dest` parameters. If no `dest` is provided then the `local` target will be preffered.* + +~~Type: `String`~~ +~~Default value: ``~~ + +~~A string value that represents the default target for the tasks. You can easily override it using the `--target` option~~ + +## Security + +For obvious reasons you may wish to keep your SSH and DB creds outside of source control. A simple way to achieve this is to store your credentials in an external file which is not tracked into VCS. + +I prefer to utilise a `.json` or `.yaml` file to store your targets as [Grunt provides methods](http://gruntjs.com/api/grunt.file#grunt.file.readjson) to parse both formats. + +### Example JSON file + +```json +{ + "local": { + "title": "Local", + "database": "local_db_name", + "user": "local_db_username", + "pass": "local_db_password", + "host": "local_db_host", + "url": "local_db_url", + }, + "develop": { + "title": "Development", + "database": "development_db_name", + "user": "development_db_username", + "pass": "development_db_password", + "host": "development_db_host", + "url": "development_db_url", + }, +} +``` + +### Usage in Grunt config +These can then be read into your Grunt config and utilised as required. + +```js +grunt.initConfig({ + // Read external file into Grunt and parse as JSON + untracked_targets: grunt.file.readJSON('untracked_targets.json'), + + deployments: { + options: { + // options here + }, + local: '<%= untracked_targets.local %>', // reuse externally defined creds + develop: '<%= untracked_targets.develop %>' // reuse externally defined creds + }, +}); +``` -A string value that represents the default target for the tasks. You can easily override it using the `--target` option +Be sure to exclude your `.json` file from your version control system of choice. ## Contributing -Contributions to this plugin are most welcome. This is very much a Alpha release and so if you find a problem please consider raising a pull request or creating a Issue which describes the problem you are having and proposes a solution. +Contributions to this plugin are most welcome. Pull requests are preferred but input on open Issues is also most agreeable! + +I still consider this a Beta release and so if you find a problem please help me out by raising a pull request or creating a Issue which describes the problem you are having and proposes a solution. + +### Testing +This project uses [Vows](http://vowsjs.org/) for BDD testing. Run the tests via Grunt using + +```` +grunt test +```` + +New features should pass all current tests and add new tests as required. Please feel free to contribute new/improved tests. + + + +### Branches and merge strategy +All pull requests should merged into the `develop` branch. __Please do not merge into the `master` branch__. In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). +## Update Guide + +### v0.4.0 + +As of `v0.4.0` the task has received several major updates. If you have used an older version of the Plugin then it's really easy to upgrade. Please check the following: + +1) You should only utilise the `db_pull` command. +2) `--target` is no longer a valid CLI parameter. Instead please pass `--src` and `--dest` which match those defined in your Grunt config. +3) In your Grunt config, check that you have defined `ssh_user` and `ssh_host` separately. `ssh_host` is now only the actual hostname. The `ssh_user` option is provided separately to accept your SSH username (see docs), +4) You are no longer forced to utilise a "Local" target. However we still advise defining one (see docs). + +If you notice any other issues please raise and issue or submit a valid pull request. + ## Release History +* 2014-01-03   v0.4.0 Major updates to streamline task. See "Update Guide" above. +* 2013-12-09   v0.3.0 Added `ignoreTables` option. * 2013-11-12   v0.2.0   Fix escaping issues, ability to define `target` via options, README doc fixes, pass host param to mysqldump. * 2013-06-11   v0.1.0   Minor updates to docs including addtion of Release History section. * 2013-06-11   v0.0.1   Initial Plugin release. diff --git a/lib/dbDump.js b/lib/dbDump.js new file mode 100644 index 0000000..6e80483 --- /dev/null +++ b/lib/dbDump.js @@ -0,0 +1,83 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var tpls = require('../lib/tpls'); + + +/** + * Dumps a MYSQL database to a suitable backup location + */ +function dbDump(config, output_paths, noExec) { + + var cmd; + var ignoreTables; + var rtn; + + grunt.file.mkdir(output_paths.dir); + + // 1) Get array of tables to ignore, as defined in the config, and format it correctly + if( config.ignoreTables ) { + ignoreTables = '--ignore-table=' + config.database + "." + config.ignoreTables.join(' --ignore-table='+config.database+'.'); + } + + // 2) Compile MYSQL cmd via Lo-Dash template string + var tpl_mysqldump = grunt.template.process(tpls.mysqldump, { + data: { + user: config.user, + pass: config.pass, + database: config.database, + host: config.host, + port: config.port || 3306, + ignoreTables: ignoreTables || '' + } + }); + + // 3) Test whether MYSQL DB requires remote access via SSH + grunt.log.writeln("Creating dump of " + config.title + " database"); + + if (typeof config.ssh_host === "undefined") { // we're not using SSH + + cmd = tpl_mysqldump; + + } else { // it's a remote SSH connection + + var tpl_ssh = grunt.template.process(tpls.ssh, { + data: { + user: config.ssh_user, + host: config.ssh_host, + port: (typeof config.ssh_port === "undefined") ? '22' : config.ssh_port + } + }); + + cmd = tpl_ssh + " \\ " + tpl_mysqldump; + } + + if (!noExec) { + // Capture output... + var output = shell.exec(cmd, {silent: true}).output; + // TODO: Add test here to check whether we were able to connect + + // Write output to file using native Grunt methods + grunt.file.write( output_paths.file, output ); + } + + if ( grunt.file.exists(output_paths.file) ) { + grunt.log.oklns("Database dump succesfully exported to: " + output_paths.file); + } else if (noExec) { + grunt.log.oklns("Running with 'noExec' option. Database dump would have otherwise been succesfully exported to: " + output_paths.file); + } else { + grunt.fail.warn("Unable to locate database dump .sql file at " + output_paths.file, 6); + } + + rtn = { + cmd: cmd, + output_file: output_paths.file + }; + + // Return for reference and test suite purposes + return rtn; + +} + +module.exports = dbDump; \ No newline at end of file diff --git a/lib/dbImport.js b/lib/dbImport.js new file mode 100644 index 0000000..ac4755b --- /dev/null +++ b/lib/dbImport.js @@ -0,0 +1,61 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var tpls = require('../lib/tpls'); + +/** + * Imports a .sql file into the DB provided + */ +function dbImport(config, src) { + + var cmd; + var rtn; + + // 1) Create cmd string from Lo-Dash template + var tpl_mysql = grunt.template.process(tpls.mysql, { + data: { + host: config.host, + user: config.user, + pass: config.pass, + database: config.database, + path: src, + port: config.port || 3306 + } + }); + + + // 2) Test whether target MYSQL DB requires remote access via SSH + grunt.log.writeln("Importing into " + config.title + " database"); + + if (typeof config.ssh_host === "undefined") { // we're not using SSH + + cmd = tpl_mysql + " < " + src; + + } else { // it's a remote SSH connection + var tpl_ssh = grunt.template.process(tpls.ssh, { + data: { + user: config.ssh_user, + host: config.ssh_host, + port: (typeof config.ssh_port === "undefined") ? '22' : config.ssh_port + } + }); + + cmd = tpl_ssh + " '" + tpl_mysql + "' < " + src; + } + + // Execute cmd + shell.exec(cmd); + + grunt.log.oklns("Database imported succesfully"); + + + rtn = { + cmd: cmd + }; + + // Return for reference and test suite purposes + return rtn; +} + +module.exports = dbImport; \ No newline at end of file diff --git a/lib/dbReplace.js b/lib/dbReplace.js new file mode 100644 index 0000000..41d0c37 --- /dev/null +++ b/lib/dbReplace.js @@ -0,0 +1,38 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var fs = require('fs-extra'); +var tpls = require('../lib/tpls'); + +function dbReplace(search, replace, output_path, noExec) { + + // Copy DB dump .sql to temp directory + // avoids overwriting original backup + grunt.file.copy(output_path.file, output_path["file-tmp"]); + + // Perform Search and replace on the temp file (not original) + var cmd = grunt.template.process( tpls.search_replace, { + data: { + search: search, + replace: replace, + path: output_path["file-tmp"] + } + }); + + if ( grunt.file.exists(output_path["file-tmp"])) { + // Execute cmd + if (!noExec) { + grunt.log.writeln("Replacing '" + search + "' with '" + replace + "' in the export (.sql) path.file."); + shell.exec(cmd); + grunt.log.oklns("Database references succesfully updated."); + } + } else { + grunt.fail.warn("Search & Replace was unable to locate database dump .sql file (expected at: " + output_path.file + ").", 6); + } + + // Return for reference and test suite purposes + return cmd; +} + +module.exports = dbReplace; \ No newline at end of file diff --git a/lib/generateBackupPaths.js b/lib/generateBackupPaths.js new file mode 100644 index 0000000..f0ac45e --- /dev/null +++ b/lib/generateBackupPaths.js @@ -0,0 +1,32 @@ +'use strict'; + +var grunt = require('grunt'); +var shell = require('shelljs'); +var tpls = require('../lib/tpls'); + + +function generateBackupPaths(target, task_options) { + + var rtn = []; + + var backups_dir = task_options['backups_dir'] || "backups"; + + // Create suitable backup directory paths + rtn['dir'] = grunt.template.process(tpls.backup_path, { + data: { + backups_dir: backups_dir, + env: target, + date: grunt.template.today('yyyymmdd'), + time: grunt.template.today('HH-MM-ss'), + } + }); + + + rtn['file'] = rtn['dir'] + '/db_backup.sql'; + rtn['dir-tmp'] = './tmp'; + rtn['file-tmp'] = rtn['dir-tmp'] + '/db_backup.sql'; + + return rtn; +} + +module.exports = generateBackupPaths; \ No newline at end of file diff --git a/lib/tpls.js b/lib/tpls.js new file mode 100644 index 0000000..ab12936 --- /dev/null +++ b/lib/tpls.js @@ -0,0 +1,20 @@ +/** + * Lo-Dash Template Helpers + * http://lodash.com/docs/#template + * https://github.com/gruntjs/grunt/wiki/grunt.template + */ +var tpls = { + + search_replace: "sed -i '' 's#<%= search %>#<%= replace %>#g' <%= path %>", + + backup_path: "<%= backups_dir %>/<%= env %>/<%= date %>/<%= time %>", + + mysql: "mysql -h <%= host %> -u <%= user %> -p<%= pass %> -P<%= port %> <%= database %>", + + ssh: "ssh -p <%= port %> <%= user %>@<%= host %>", + + mysqldump: "mysqldump -h <%= host %> -u<%= user %> -p<%= pass %> -P<%= port %> <%= database %> <%= ignoreTables %>" +}; + + +module.exports = tpls; \ No newline at end of file diff --git a/package.json b/package.json index ffa1885..850c71c 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,17 @@ "test": "grunt test" }, "dependencies": { - "shelljs": "~0.1.4" + "shelljs": "~0.2.6", + "fs-extra": "~0.8.1" }, "devDependencies": { - "grunt-contrib-jshint": "~0.1.1", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-nodeunit": "~0.1.2", - "grunt": "~0.4.1" + "grunt": "~0.4.1", + "grunt-contrib-jshint": "~0.8.0", + "grunt-contrib-clean": "~0.5.0", + "matchdep": "~0.1.2", + "grunt-vows": "~0.4.0", + "vows": "~0.7.0", + "grunt-contrib-watch": "~0.5.0" }, "peerDependencies": { "grunt": "~0.4.1" @@ -48,4 +52,4 @@ "mysql", "wordpress" ] -} \ No newline at end of file +} diff --git a/tasks/deployments.js b/tasks/deployments.js index 38b2ceb..74cadb9 100644 --- a/tasks/deployments.js +++ b/tasks/deployments.js @@ -8,12 +8,20 @@ 'use strict'; +// Global var shell = require('shelljs'); +var fs = require('fs-extra'); -module.exports = function(grunt) { - +// Library modules +var tpls = require('../lib/tpls'); +var dbReplace = require('../lib/dbReplace'); +var dbDump = require('../lib/dbDump'); +var dbImport = require('../lib/dbImport'); +var generateBackupPaths = require('../lib/generateBackupPaths'); +// Only Grunt registration within this "exports" +module.exports = function(grunt) { /** * DB PUSH @@ -36,24 +44,24 @@ module.exports = function(grunt) { var local_options = grunt.config.get('deployments').local; // Generate required backup directories and paths - var local_backup_paths = generate_backup_paths("local", task_options); - var target_backup_paths = generate_backup_paths(target, task_options); + var local_backup_paths = generateBackupPaths("local", task_options); + var target_backup_paths = generateBackupPaths(target, task_options); grunt.log.subhead("Pushing database from 'Local' to '" + target_options.title + "'"); // Dump local DB - db_dump(local_options, local_backup_paths); + dbDump(local_options, local_backup_paths); // Search and Replace database refs - db_replace( local_options.url, target_options.url, local_backup_paths.file ); + dbReplace( local_options.url, target_options.url, local_backup_paths.file ); // Dump target DB - db_dump(target_options, target_backup_paths); + dbDump(target_options, target_backup_paths); // Import dump to target DB - db_import(target_options, local_backup_paths.file); + dbImport(target_options, local_backup_paths.file); grunt.log.subhead("Operations completed"); }); @@ -66,198 +74,62 @@ module.exports = function(grunt) { grunt.registerTask('db_pull', 'Pull from Database', function() { // Options - var task_options = grunt.config.get('deployments')['options']; + var task_options = grunt.config.get('deployments')['options']; + + // Get the source from the CLI args + var src = grunt.option('src') || task_options['src']; + if ( typeof src === "undefined") { + grunt.fail.warn("Invalid source provided. I cannot pull a database from nowhere! Please checked your CLI 'dest' argument is valid.", 6); + } else if (typeof grunt.config.get('deployments')[src] === "undefined") { + grunt.fail.warn("Invalid source provided. I cannot pull a database from nowhere! Please checked your Grunt task configuration.", 6); + } - // Get the target from the CLI args - var target = grunt.option('target') || task_options['target']; + // Get the destination from the CLI args + var dest = grunt.option('dest') || task_options['dest']; - if ( typeof target === "undefined" || typeof grunt.config.get('deployments')[target] === "undefined") { - grunt.fail.warn("Invalid target provided. I cannot pull a database from nowhere! Please checked your configuration and provide a valid target.", 6); + // Default to "local" if no destination is provided + if ( typeof dest === "undefined" ) { + dest = "local"; } - + // Check destination is valid + if ( typeof grunt.config.get('deployments')[dest] === "undefined") { + grunt.fail.warn("Invalid destination provided. I cannot move a database to a non existent location! Please checked your Grunt task configuration and provide a valid destination.", 6); + } // Grab the options from the shared "deployments" config options - var target_options = grunt.config.get('deployments')[target]; - var local_options = grunt.config.get('deployments').local; + var src_options = grunt.config.get('deployments')[src]; + var dest_options = grunt.config.get('deployments')[dest]; // Generate required backup directories and paths - var local_backup_paths = generate_backup_paths("local", task_options); - var target_backup_paths = generate_backup_paths(target, task_options); + var src_backup_paths = generateBackupPaths(src, task_options); + var dest_backup_paths = generateBackupPaths(dest, task_options); // Start execution - grunt.log.subhead("Pulling database from '" + target_options.title + "' into Local"); + grunt.log.subhead("Pulling database from '" + src_options.title + "' into " + dest_options.title); - // Dump Target DB - db_dump(target_options, target_backup_paths ); + // Dump (backup) the source DB + dbDump(src_options, src_backup_paths ); - db_replace(target_options.url,local_options.url,target_backup_paths.file); + // Performance search and replace on DUMP + dbReplace(src_options.url,dest_options.url,src_backup_paths); // Backup Local DB - db_dump(local_options, local_backup_paths); + dbDump(dest_options, dest_backup_paths); // Import dump into Local - db_import(local_options,target_backup_paths.file); - - grunt.log.subhead("Operations completed"); - - }); - - - - function generate_backup_paths(target, task_options) { - - var rtn = []; + dbImport(dest_options,src_backup_paths["file-tmp"]); - var backups_dir = task_options['backups_dir'] || "backups"; - - // Create suitable backup directory paths - rtn['dir'] = grunt.template.process(tpls.backup_path, { - data: { - backups_dir: backups_dir, - env: target, - date: grunt.template.today('yyyymmdd'), - time: grunt.template.today('HH-MM-ss'), - } - }); - - - rtn['file'] = rtn['dir'] + '/db_backup.sql'; - - return rtn; - } - - - /** - * Imports a .sql file into the DB provided - */ - function db_import(config, src) { - - var cmd; - - // 1) Create cmd string from Lo-Dash template - var tpl_mysql = grunt.template.process(tpls.mysql, { - data: { - host: config.host, - user: config.user, - pass: config.pass, - database: config.database, - path: src + // Clean up tmp directory + fs.removeSync(src_backup_paths["dir-tmp"], function (err) { + if (err) { + throw err; } }); + grunt.log.subhead("Operations completed"); - // 2) Test whether target MYSQL DB is local or whether requires remote access via SSH - if (typeof config.ssh_host === "undefined") { // it's a local connection - grunt.log.writeln("Importing into local database"); - cmd = tpl_mysql + " < " + src; - } else { // it's a remote connection - var tpl_ssh = grunt.template.process(tpls.ssh, { - data: { - host: config.ssh_host - } - }); - - grunt.log.writeln("Importing DUMP into remote database"); - - cmd = tpl_ssh + " '" + tpl_mysql + "' < " + src; - } - - // Execute cmd - shell.exec(cmd); - - grunt.log.oklns("Database imported succesfully"); - } - - - - /** - * Dumps a MYSQL database to a suitable backup location - */ - function db_dump(config, output_paths) { - - var cmd; - - grunt.file.mkdir(output_paths.dir); - - - // 2) Compile MYSQL cmd via Lo-Dash template string - var tpl_mysqldump = grunt.template.process(tpls.mysqldump, { - data: { - user: config.user, - pass: config.pass, - database: config.database, - host: config.host - } - }); - - - // 3) Test whether MYSQL DB is local or whether requires remote access via SSH - if (typeof config.ssh_host === "undefined") { // it's a local connection - grunt.log.writeln("Creating DUMP of local database"); - cmd = tpl_mysqldump; - - } else { // it's a remote connection - var tpl_ssh = grunt.template.process(tpls.ssh, { - data: { - host: config.ssh_host - } - }); - grunt.log.writeln("Creating DUMP of remote database"); - - cmd = tpl_ssh + " \\ " + tpl_mysqldump; - } - - // Capture output... - var output = shell.exec(cmd, {silent: true}).output; - - // Write output to file using native Grunt methods - grunt.file.write( output_paths.file, output ); - - grunt.log.oklns("Database DUMP succesfully exported to: " + output_paths.file); - - } - - - function db_replace(search,replace,output_file) { - - var cmd = grunt.template.process(tpls.search_replace, { - data: { - search: search, - replace: replace, - path: output_file - } - }); - - grunt.log.writeln("Replacing '" + search + "' with '" + replace + "' in the database."); - // Execute cmd - shell.exec(cmd); - grunt.log.oklns("Database references succesfully updated."); - } - - - - - /** - * Lo-Dash Template Helpers - * http://lodash.com/docs/#template - * https://github.com/gruntjs/grunt/wiki/grunt.template - */ - var tpls = { - - backup_path: "<%= backups_dir %>/<%= env %>/<%= date %>/<%= time %>", - - search_replace: "sed -i '' 's#<%= search %>#<%= replace %>#g' <%= path %>", - - mysqldump: "mysqldump -h <%= host %> -u<%= user %> -p<%= pass %> <%= database %>", - - mysql: "mysql -h <%= host %> -u <%= user %> -p<%= pass %> <%= database %>", - - ssh: "ssh <%= host %>", - }; - - - + }); }; diff --git a/test/deployments_test.js b/test/deployments_test.js index 0632799..e69de29 100644 --- a/test/deployments_test.js +++ b/test/deployments_test.js @@ -1,48 +0,0 @@ -'use strict'; - -var grunt = require('grunt'); - -/* - ======== A Handy Little Nodeunit Reference ======== - https://github.com/caolan/nodeunit - - Test methods: - test.expect(numAssertions) - test.done() - Test assertions: - test.ok(value, [message]) - test.equal(actual, expected, [message]) - test.notEqual(actual, expected, [message]) - test.deepEqual(actual, expected, [message]) - test.notDeepEqual(actual, expected, [message]) - test.strictEqual(actual, expected, [message]) - test.notStrictEqual(actual, expected, [message]) - test.throws(block, [error], [message]) - test.doesNotThrow(block, [error], [message]) - test.ifError(value) -*/ - -exports.deployments = { - setUp: function(done) { - // setup here if necessary - done(); - }, - default_options: function(test) { - test.expect(1); - - var actual = grunt.file.read('tmp/default_options'); - var expected = grunt.file.read('test/expected/default_options'); - test.equal(actual, expected, 'should describe what the default behavior is.'); - - test.done(); - }, - custom_options: function(test) { - test.expect(1); - - var actual = grunt.file.read('tmp/custom_options'); - var expected = grunt.file.read('test/expected/custom_options'); - test.equal(actual, expected, 'should describe what the custom option(s) behavior is.'); - - test.done(); - }, -}; diff --git a/test/fixtures/basic_config.json b/test/fixtures/basic_config.json new file mode 100644 index 0000000..0b9abcd --- /dev/null +++ b/test/fixtures/basic_config.json @@ -0,0 +1,20 @@ +{ + "local": { + "title": "Local", + "database": "deploy_test", + "user": "root", + "pass": "pass4burfield", + "host": "localhost", + "url": "www.deploytest.dev:8888" + }, + "develop": { + "title": "Development Server", + "database": "ddeploy_dev", + "user": "ddeploy_dev", + "pass": "test4test", + "host": "localhost", + "url": "deploytest.com.burfield-dev.com", + "ssh_user": "ddeploy", + "ssh_host": "134.0.18.114" + } +} diff --git a/test/options_test.js b/test/options_test.js new file mode 100644 index 0000000..6eb0d56 --- /dev/null +++ b/test/options_test.js @@ -0,0 +1,20 @@ +/* 'use strict'; + +var vows = require("vows"), + assert = require("assert"), + mysqldumpwrapper = require('../lib/mysqldumpwrapper.js'); + + +exports.suite = vows.describe("Package options tests").addBatch({ + "The User option": { + topic: function() { + mysqldumpwrapper({ + + }); + }, + "errors if undefined or empty": function (topic) { + assert.throws(topic, Error); + } + } +}); + */ \ No newline at end of file diff --git a/test/package_test.js b/test/package_test.js new file mode 100644 index 0000000..0efc6bb --- /dev/null +++ b/test/package_test.js @@ -0,0 +1,56 @@ +'use strict'; + +var grunt = require('grunt'), + fs = require("fs"), + vows = require("vows"), + assert = require("assert"), + dbReplace = require('../lib/dbReplace'), + dbDump = require('../lib/dbDump'), + generateBackupPaths = require('../lib/generateBackupPaths'), + basic_config = grunt.file.readJSON('test/fixtures/basic_config.json'); + + + +exports.suite = vows.describe("Basic tests").addBatch({ + "The dbReplace task": { + topic: dbReplace("foo","bar","test-file.txt", true), + "is not null": function (topic) { + assert.isNotNull(topic); + }, + "command is composed correctly": function (topic) { + assert.equal(topic, "sed -i '' 's#foo#bar#g' test-file.txt"); + }, + // add test to check against a fixture MYSQL export file that a search and replace works as expected + } +}).addBatch({ + "The dbDump task": { + topic: dbDump( + basic_config.local, + generateBackupPaths("local",{}) + ), + "is not null": function (topic) { + assert.isNotNull(topic); + }, + "command is composed correctly using basic data": function (topic) { + assert.equal(topic.cmd.trim(), "mysqldump -h localhost -uroot -ppass4burfield -P3306 deploy_test"); + }, + "results in a .sql file that": { + topic: function (topic) { + fs.stat(topic.output_file, this.callback); + }, + "has contents": function (err, stat) { + assert.isNull(err); + assert.isNotZero(stat.size); + } + } + } +}); + + + + + + + + +