Skip to content

Commit 860cf18

Browse files
committed
fix(mysql): cleanup promise structure of mysql extension
closes #191 - updates mysql extension to use listr - cleans up promise and error structure - use new ui.logVerbose method to log statuses
1 parent 09ec161 commit 860cf18

File tree

4 files changed

+106
-107
lines changed

4 files changed

+106
-107
lines changed

extensions/mysql/index.js

+87-97
Original file line numberDiff line numberDiff line change
@@ -7,129 +7,119 @@ const omit = require('lodash/omit');
77
const cli = require('../../lib');
88

99
class MySQLExtension extends cli.Extension {
10-
_query(queryString) {
11-
return Promise.fromCallback(cb => this.connection.query(queryString, cb));
12-
}
13-
1410
setup(cmd, argv) {
1511
// ghost setup --local, skip
1612
if (argv.local) {
1713
return;
1814
}
1915

20-
cmd.addStage('mysql', this.setupMySQL.bind(this));
16+
cmd.addStage('mysql', this.setupMySQL.bind(this), [], 'a ghost mysql user');
2117
}
2218

2319
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(() => {
20+
let dbconfig = ctx.instance.config.get('database.connection');
21+
22+
if (dbconfig.user !== 'root') {
23+
this.ui.log('MySQL user is not root, skipping additional user setup', 'yellow');
24+
return task.skip();
25+
}
26+
27+
return this.ui.listr([{
28+
title: 'Connecting to database',
29+
task: () => this.canConnect(ctx, dbconfig)
30+
}, {
31+
title: 'Creating new MySQL user',
32+
task: () => this.createUser(ctx, dbconfig)
33+
}, {
34+
title: 'Granting new user permissions',
35+
task: () => this.grantPermissions(ctx, dbconfig)
36+
}, {
37+
title: 'Finishing up',
38+
task: () => {
39+
ctx.instance.config.set('database.connection.user', ctx.mysql.username)
40+
.set('database.connection.password', ctx.mysql.password).save();
41+
4042
this.connection.end();
41-
});
43+
}
44+
}], false);
4245
}
4346

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-
config: {
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,
47+
canConnect(ctx, dbconfig) {
48+
this.connection = mysql.createConnection(omit(dbconfig, 'database'));
49+
50+
return Promise.fromCallback(cb => this.connection.connect(cb)).catch((error) => {
51+
if (error.code === 'ECONNREFUSED') {
52+
return Promise.reject(new cli.errors.ConfigError({
53+
message: error.message,
54+
config: {
55+
'database.connection.host': dbconfig.host,
56+
'database.connection.port': dbconfig.port || '3306'
57+
},
58+
environment: this.system.environment,
59+
help: 'Please ensure that MySQL is installed and reachable. You can always re-run `ghost setup` to try again.'
60+
}));
61+
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
62+
return Promise.reject(new cli.errors.ConfigError({
63+
message: error.message,
6864
config: {
69-
'database.connection.host': this.databaseConfig.connection.host,
70-
'database.connection.port': this.databaseConfig.connection.port || '3306'
65+
'database.connection.user': dbconfig.user,
66+
'database.connection.password': dbconfig.password
7167
},
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-
});
68+
environment: this.system.environment,
69+
help: 'You can run `ghost config` to re-enter the correct credentials. Alternatively you can run `ghost setup` again.'
70+
}));
71+
}
72+
73+
return Promise.reject(error);
74+
});
7675
}
7776

78-
createMySQLUser(ctx) {
77+
createUser(ctx, dbconfig) {
7978
let randomPassword = crypto.randomBytes(10).toString('hex');
80-
let host = this.databaseConfig.connection.host;
8179

8280
// 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
81+
// 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 work
8482
// e.g. if we would rely on the instance name, the instance naming only auto increments if there are existing instances
8583
// 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
8684
// 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)
8785
let username = 'ghost-' + Math.floor(Math.random() * 1000);
8886

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-
});
87+
return this._query(`CREATE USER '${username}'@'${dbconfig.host}' IDENTIFIED BY '${randomPassword}';`).then(() => {
88+
this.ui.logVerbose(`MySQL: successfully created new user ${username}`, 'green');
89+
90+
ctx.mysql = {
91+
username: username,
92+
password: randomPassword
93+
};
94+
}).catch((error) => {
95+
// User already exists, run this method again
96+
if (error.errno === 1396) {
97+
this.ui.logVerbose('MySQL: user exists, re-trying user creation with new username', 'yellow');
98+
return this.createUser(ctx, dbconfig);
99+
}
100+
101+
this.ui.logVerbose('MySQL: Unable to create custom Ghost user', 'red');
102+
this.connection.end(); // Ensure we end the connection
103+
return Promise.reject(new cli.errors.SystemError(`Creating new mysql user errored with message: ${error.message}`));
104+
});
109105
}
110106

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-
});
107+
grantPermissions(ctx, dbconfig) {
108+
return this._query(`GRANT ALL PRIVILEGES ON ${dbconfig.database}.* TO '${ctx.mysql.username}'@'${dbconfig.host}';`).then(() => {
109+
this.ui.logVerbose(`MySQL: Successfully granted privileges for user ${ctx.mysql.username}`, 'green');
110+
return this._query('FLUSH PRIVILEGES;');
111+
}).then(() => {
112+
this.ui.logVerbose('MySQL: flushed privileges', 'green');
113+
}).catch((error) => {
114+
this.ui.logVerbose('MySQL: Unable either to grant permissions or flush privileges', 'red');
115+
this.connection.end();
116+
return Promise.reject(new cli.errors.SystemError(`Granting database permissions errored with message: ${error.message}`));
117+
});
118+
}
119+
120+
_query(queryString) {
121+
this.ui.logVerbose(`MySQL: running query > ${queryString}`, 'gray');
122+
return Promise.fromCallback(cb => this.connection.query(queryString, cb));
133123
}
134124
}
135125

extensions/mysql/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@
55
"keywords": [
66
"ghost-cli-extension"
77
],
8-
"main": "index.js"
8+
"main": "index.js",
9+
"ghost-cli": {
10+
"before": "ghost-cli-linux"
11+
}
912
}

lib/tasks/migrate.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ module.exports = function runMigrations(context) {
3030
'database.connection.host': config.get('database.connection.host')
3131
},
3232
message: 'Invalid database host',
33-
environment: context.environment
33+
environment: context.instance.system.environment
3434
});
3535
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
3636
error = new errors.ConfigError({
3737
config: {
3838
'database.connection.user': config.get('database.connection.user'),
39-
'database.connection.password': config.get('database.connection.pasword')
39+
'database.connection.password': config.get('database.connection.password')
4040
},
4141
message: 'Invalid database username or password',
42-
environment: context.environment
42+
environment: context.instance.system.environment
4343
});
4444
}
4545

test/unit/tasks/migrate-spec.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,12 @@ describe('Unit: Tasks > Migrate', function () {
129129
}
130130
});
131131

132-
return migrate({ instance: { config: {
133-
get: getStub, set: setStub, save: saveStub
134-
} } }).then(() => {
132+
return migrate({ instance: {
133+
config: {
134+
get: getStub, set: setStub, save: saveStub
135+
},
136+
system: { environment: 'testing' }
137+
} }).then(() => {
135138
expect(false, 'error should have been thrown').to.be.true;
136139
}).catch((error) => {
137140
expect(error).to.be.an.instanceof(errors.ConfigError);
@@ -155,9 +158,12 @@ describe('Unit: Tasks > Migrate', function () {
155158
}
156159
});
157160

158-
return migrate({ instance: { config: {
159-
get: getStub, set: setStub, save: saveStub
160-
} } }).then(() => {
161+
return migrate({ instance: {
162+
config: {
163+
get: getStub, set: setStub, save: saveStub
164+
},
165+
system: { environment: 'testing' }
166+
} }).then(() => {
161167
expect(false, 'error should have been thrown').to.be.true;
162168
}).catch((error) => {
163169
expect(error).to.be.an.instanceof(errors.ConfigError);

0 commit comments

Comments
 (0)