Skip to content

Commit

Permalink
feat(mysql): mysql user creation extension (#269)
Browse files Browse the repository at this point in the history
refs #191 
- adds initial iteration of mysql user extension
- adds some config error modifications to output multiple config values
  • Loading branch information
kirrg001 authored and acburdine committed Jul 5, 2017
1 parent 8aac988 commit 78f8439
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 4 deletions.
136 changes: 136 additions & 0 deletions extensions/mysql/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use strict';

const Promise = require('bluebird');
const mysql = require('mysql');
const crypto = require('crypto');
const omit = require('lodash/omit');
const cli = require('../../lib');

class MySQLExtension extends cli.Extension {
_query(queryString) {
return Promise.fromCallback(cb => this.connection.query(queryString, cb));
}

setup(cmd, argv) {
// ghost setup --local, skip
if (argv.local) {
return;
}

cmd.addStage('mysql', this.setupMySQL.bind(this));
}

setupMySQL(argv, ctx, task) {
this.databaseConfig = ctx.instance.config.get('database');

return this.canConnect(ctx)
.then(() => {
if (this.databaseConfig.connection.user === 'root') {
return this.ui.confirm('Your MySQL user is root. Would you like to create a custom Ghost MySQL user?', true)
.then((res) => {
if (res.yes) {
return this.createMySQLUser(ctx);
}
});
}

this.ui.log('MySQL: Your user is: ' + this.databaseConfig.connection.user, 'green');
})
.finally(() => {
this.connection.end();
});
}

canConnect(ctx) {
this.connection = mysql.createConnection(omit(this.databaseConfig.connection, 'database'));

return Promise.fromCallback(cb => this.connection.connect(cb))
.then(() => {
this.ui.log('MySQL: connection successful.', 'green');
})
.catch((err) => {
this.ui.log('MySQL: connection error.', 'yellow');

if (err.code === 'ER_ACCESS_DENIED_ERROR') {
throw new cli.errors.ConfigError({
message: err.message,
configs: {
'database.connection.user': this.databaseConfig.connection.user,
'database.connection.password': this.databaseConfig.connection.password
},
environment: ctx.instance.system.environment,
help: 'You can run `ghost config` to re-enter the correct credentials. Alternatively you can run `ghost setup` again.'
});
}

throw new cli.errors.ConfigError({
message: err.message,
configs: {
'database.connection.host': this.databaseConfig.connection.host,
'database.connection.port': this.databaseConfig.connection.port || '3306'
},
environment: ctx.instance.system.environment,
help: 'Please ensure that MySQL is installed and reachable. You can always re-run `ghost setup` and try it again.'
});
});
}

createMySQLUser(ctx) {
let randomPassword = crypto.randomBytes(10).toString('hex');
let host = this.databaseConfig.connection.host;

// IMPORTANT: we generate random MySQL usernames
// e.g. you delete all your Ghost instances from your droplet and start from scratch, the MySQL users would remain and the CLI has to generate a random user name to be able to
// e.g. if we would rely on the instance name, the instance naming only auto increments if there are existing instances
// the most important fact is, that if a MySQL user exists, we have no access to the password, which we need to autofill the Ghost config
// disadvantage: the CLI could potentially create lot's of MySQL users (but this should only happen if the user installs Ghost over and over again with root credentials)
let username = 'ghost-' + Math.floor(Math.random() * 1000);

return this._query('CREATE USER \'' + username + '\'@\'' + host + '\' IDENTIFIED BY \'' + randomPassword + '\';')
.then(() => {
this.ui.log('MySQL: successfully created `' + username + '`.', 'green');

return this.grantPermissions({username: username})
.then(() => {
ctx.instance.config.set('database.connection.user', username);
ctx.instance.config.set('database.connection.password', randomPassword);
});
})
.catch((err) => {
// CASE: user exists, we are not able to figure out the original password, skip mysql setup
if (err.errno === 1396) {
this.ui.log('MySQL: `' + username + '` user exists. Skipping.', 'yellow');
return Promise.resolve();
}

this.ui.log('MySQL: unable to create custom Ghost user.', 'yellow');
throw new cli.errors.SystemError(err.message);
});
}

grantPermissions(options) {
let host = this.databaseConfig.connection.host;
let database = this.databaseConfig.connection.database;
let username = options.username;

return this._query('GRANT ALL PRIVILEGES ON ' + database + '.* TO \'' + username + '\'@\'' + host + '\';')
.then(() => {
this.ui.log('MySQL: successfully granted permissions for `' + username + '` user.', 'green');

return this._query('FLUSH PRIVILEGES;')
.then(() => {
this.ui.log('MySQL: flushed privileges', 'green');
})
.catch((err) => {
this.ui.log('MySQL: unable to flush privileges.', 'yellow');
throw new cli.errors.SystemError(err.message);
});
})
.catch((err) => {
this.ui.log('MySQL: unable to grant permissions for `' + username + '` user.', 'yellow');
throw new cli.errors.SystemError(err.message);
});
}
}

module.exports = MySQLExtension;
5 changes: 3 additions & 2 deletions extensions/mysql/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "ghost-cli-config-mysql",
"name": "ghost-cli-mysql",
"version": "0.0.0",
"description": "MySQL configuration handling for Ghost-CLI",
"keywords": [
"ghost-cli-extension"
]
],
"main": "index.js"
}
19 changes: 18 additions & 1 deletion lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ class ProcessError extends CliError {
* @class GhostError
* @extends CliError
*/
class GhostError extends CliError {}
class GhostError extends CliError {
}

/**
* Handles all errors resulting from system issues
Expand Down Expand Up @@ -133,10 +134,26 @@ class ConfigError extends CliError {
let initial = chalk.red(`Error detected in the ${this.options.environment} configuration.\n\n`) +
`${chalk.gray('Message:')} ${this.options.message}\n`;

// @TODO: merge this solution into one
if (this.options.configKey) {
initial += `${chalk.gray('Configuration Key:')} ${this.options.configKey}\n` +
`${chalk.gray('Current Value:')} ${this.options.configValue}\n\n` +
chalk.blue(`Run \`${chalk.underline(`ghost config ${this.options.configKey} <new value>`)}\` to fix it.\n`);
} else if (this.options.configs) {
initial += '\n';

for (const key in this.options.configs) {
if (this.options.configs.hasOwnProperty(key)) {
initial += `${chalk.gray('Configuration Key:')} ${key}\n` +
`${chalk.gray('Current Value:')} ${this.options.configs[key]}\n\n`;
}
}

if (this.options.help) {
initial += '\n';
initial += `${chalk.gray('Help:')} ${this.options.help}`;
initial += '\n';
}
}

return initial;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"knex-migrator": "2.0.16",
"listr": "0.12.0",
"lodash": "4.17.4",
"mysql": "2.13.0",
"nginx-conf": "1.3.0",
"ora": "1.3.0",
"portfinder": "1.0.13",
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2449,7 +2449,7 @@ mv@~2:
ncp "~2.0.0"
rimraf "~2.4.0"

mysql@^2.11.1:
mysql@2.13.0, mysql@^2.11.1:
version "2.13.0"
resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.13.0.tgz#998f1f8ca46e2e3dd7149ce982413653986aae47"
dependencies:
Expand Down

0 comments on commit 78f8439

Please sign in to comment.