Skip to content

Commit 78f8439

Browse files
kirrg001acburdine
authored andcommitted
feat(mysql): mysql user creation extension (#269)
refs #191 - adds initial iteration of mysql user extension - adds some config error modifications to output multiple config values
1 parent 8aac988 commit 78f8439

File tree

5 files changed

+159
-4
lines changed

5 files changed

+159
-4
lines changed

extensions/mysql/index.js

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use strict';
2+
3+
const Promise = require('bluebird');
4+
const mysql = require('mysql');
5+
const crypto = require('crypto');
6+
const omit = require('lodash/omit');
7+
const cli = require('../../lib');
8+
9+
class MySQLExtension extends cli.Extension {
10+
_query(queryString) {
11+
return Promise.fromCallback(cb => this.connection.query(queryString, cb));
12+
}
13+
14+
setup(cmd, argv) {
15+
// ghost setup --local, skip
16+
if (argv.local) {
17+
return;
18+
}
19+
20+
cmd.addStage('mysql', this.setupMySQL.bind(this));
21+
}
22+
23+
setupMySQL(argv, ctx, task) {
24+
this.databaseConfig = ctx.instance.config.get('database');
25+
26+
return this.canConnect(ctx)
27+
.then(() => {
28+
if (this.databaseConfig.connection.user === 'root') {
29+
return this.ui.confirm('Your MySQL user is root. Would you like to create a custom Ghost MySQL user?', true)
30+
.then((res) => {
31+
if (res.yes) {
32+
return this.createMySQLUser(ctx);
33+
}
34+
});
35+
}
36+
37+
this.ui.log('MySQL: Your user is: ' + this.databaseConfig.connection.user, 'green');
38+
})
39+
.finally(() => {
40+
this.connection.end();
41+
});
42+
}
43+
44+
canConnect(ctx) {
45+
this.connection = mysql.createConnection(omit(this.databaseConfig.connection, 'database'));
46+
47+
return Promise.fromCallback(cb => this.connection.connect(cb))
48+
.then(() => {
49+
this.ui.log('MySQL: connection successful.', 'green');
50+
})
51+
.catch((err) => {
52+
this.ui.log('MySQL: connection error.', 'yellow');
53+
54+
if (err.code === 'ER_ACCESS_DENIED_ERROR') {
55+
throw new cli.errors.ConfigError({
56+
message: err.message,
57+
configs: {
58+
'database.connection.user': this.databaseConfig.connection.user,
59+
'database.connection.password': this.databaseConfig.connection.password
60+
},
61+
environment: ctx.instance.system.environment,
62+
help: 'You can run `ghost config` to re-enter the correct credentials. Alternatively you can run `ghost setup` again.'
63+
});
64+
}
65+
66+
throw new cli.errors.ConfigError({
67+
message: err.message,
68+
configs: {
69+
'database.connection.host': this.databaseConfig.connection.host,
70+
'database.connection.port': this.databaseConfig.connection.port || '3306'
71+
},
72+
environment: ctx.instance.system.environment,
73+
help: 'Please ensure that MySQL is installed and reachable. You can always re-run `ghost setup` and try it again.'
74+
});
75+
});
76+
}
77+
78+
createMySQLUser(ctx) {
79+
let randomPassword = crypto.randomBytes(10).toString('hex');
80+
let host = this.databaseConfig.connection.host;
81+
82+
// IMPORTANT: we generate random MySQL usernames
83+
// 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
84+
// e.g. if we would rely on the instance name, the instance naming only auto increments if there are existing instances
85+
// 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
86+
// 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)
87+
let username = 'ghost-' + Math.floor(Math.random() * 1000);
88+
89+
return this._query('CREATE USER \'' + username + '\'@\'' + host + '\' IDENTIFIED BY \'' + randomPassword + '\';')
90+
.then(() => {
91+
this.ui.log('MySQL: successfully created `' + username + '`.', 'green');
92+
93+
return this.grantPermissions({username: username})
94+
.then(() => {
95+
ctx.instance.config.set('database.connection.user', username);
96+
ctx.instance.config.set('database.connection.password', randomPassword);
97+
});
98+
})
99+
.catch((err) => {
100+
// CASE: user exists, we are not able to figure out the original password, skip mysql setup
101+
if (err.errno === 1396) {
102+
this.ui.log('MySQL: `' + username + '` user exists. Skipping.', 'yellow');
103+
return Promise.resolve();
104+
}
105+
106+
this.ui.log('MySQL: unable to create custom Ghost user.', 'yellow');
107+
throw new cli.errors.SystemError(err.message);
108+
});
109+
}
110+
111+
grantPermissions(options) {
112+
let host = this.databaseConfig.connection.host;
113+
let database = this.databaseConfig.connection.database;
114+
let username = options.username;
115+
116+
return this._query('GRANT ALL PRIVILEGES ON ' + database + '.* TO \'' + username + '\'@\'' + host + '\';')
117+
.then(() => {
118+
this.ui.log('MySQL: successfully granted permissions for `' + username + '` user.', 'green');
119+
120+
return this._query('FLUSH PRIVILEGES;')
121+
.then(() => {
122+
this.ui.log('MySQL: flushed privileges', 'green');
123+
})
124+
.catch((err) => {
125+
this.ui.log('MySQL: unable to flush privileges.', 'yellow');
126+
throw new cli.errors.SystemError(err.message);
127+
});
128+
})
129+
.catch((err) => {
130+
this.ui.log('MySQL: unable to grant permissions for `' + username + '` user.', 'yellow');
131+
throw new cli.errors.SystemError(err.message);
132+
});
133+
}
134+
}
135+
136+
module.exports = MySQLExtension;

extensions/mysql/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
2-
"name": "ghost-cli-config-mysql",
2+
"name": "ghost-cli-mysql",
33
"version": "0.0.0",
44
"description": "MySQL configuration handling for Ghost-CLI",
55
"keywords": [
66
"ghost-cli-extension"
7-
]
7+
],
8+
"main": "index.js"
89
}

lib/errors.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ class ProcessError extends CliError {
9898
* @class GhostError
9999
* @extends CliError
100100
*/
101-
class GhostError extends CliError {}
101+
class GhostError extends CliError {
102+
}
102103

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

137+
// @TODO: merge this solution into one
136138
if (this.options.configKey) {
137139
initial += `${chalk.gray('Configuration Key:')} ${this.options.configKey}\n` +
138140
`${chalk.gray('Current Value:')} ${this.options.configValue}\n\n` +
139141
chalk.blue(`Run \`${chalk.underline(`ghost config ${this.options.configKey} <new value>`)}\` to fix it.\n`);
142+
} else if (this.options.configs) {
143+
initial += '\n';
144+
145+
for (const key in this.options.configs) {
146+
if (this.options.configs.hasOwnProperty(key)) {
147+
initial += `${chalk.gray('Configuration Key:')} ${key}\n` +
148+
`${chalk.gray('Current Value:')} ${this.options.configs[key]}\n\n`;
149+
}
150+
}
151+
152+
if (this.options.help) {
153+
initial += '\n';
154+
initial += `${chalk.gray('Help:')} ${this.options.help}`;
155+
initial += '\n';
156+
}
140157
}
141158

142159
return initial;

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"knex-migrator": "2.0.16",
5858
"listr": "0.12.0",
5959
"lodash": "4.17.4",
60+
"mysql": "2.13.0",
6061
"nginx-conf": "1.3.0",
6162
"ora": "1.3.0",
6263
"portfinder": "1.0.13",

yarn.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -2449,7 +2449,7 @@ mv@~2:
24492449
ncp "~2.0.0"
24502450
rimraf "~2.4.0"
24512451

2452-
mysql@^2.11.1:
2452+
mysql@2.13.0, mysql@^2.11.1:
24532453
version "2.13.0"
24542454
resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.13.0.tgz#998f1f8ca46e2e3dd7149ce982413653986aae47"
24552455
dependencies:

0 commit comments

Comments
 (0)