Skip to content

Commit

Permalink
fix(mysql): cleanup promise structure of mysql extension
Browse files Browse the repository at this point in the history
closes #191
- updates mysql extension to use listr
- cleans up promise and error structure
- use new ui.logVerbose method to log statuses
  • Loading branch information
acburdine committed Jul 6, 2017
1 parent 09ec161 commit 860cf18
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 107 deletions.
184 changes: 87 additions & 97 deletions extensions/mysql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,129 +7,119 @@ 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));
cmd.addStage('mysql', this.setupMySQL.bind(this), [], 'a ghost mysql user');
}

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(() => {
let dbconfig = ctx.instance.config.get('database.connection');

if (dbconfig.user !== 'root') {
this.ui.log('MySQL user is not root, skipping additional user setup', 'yellow');
return task.skip();
}

return this.ui.listr([{
title: 'Connecting to database',
task: () => this.canConnect(ctx, dbconfig)
}, {
title: 'Creating new MySQL user',
task: () => this.createUser(ctx, dbconfig)
}, {
title: 'Granting new user permissions',
task: () => this.grantPermissions(ctx, dbconfig)
}, {
title: 'Finishing up',
task: () => {
ctx.instance.config.set('database.connection.user', ctx.mysql.username)
.set('database.connection.password', ctx.mysql.password).save();

this.connection.end();
});
}
}], false);
}

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,
config: {
'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,
canConnect(ctx, dbconfig) {
this.connection = mysql.createConnection(omit(dbconfig, 'database'));

return Promise.fromCallback(cb => this.connection.connect(cb)).catch((error) => {
if (error.code === 'ECONNREFUSED') {
return Promise.reject(new cli.errors.ConfigError({
message: error.message,
config: {
'database.connection.host': dbconfig.host,
'database.connection.port': dbconfig.port || '3306'
},
environment: this.system.environment,
help: 'Please ensure that MySQL is installed and reachable. You can always re-run `ghost setup` to try again.'
}));
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
return Promise.reject(new cli.errors.ConfigError({
message: error.message,
config: {
'database.connection.host': this.databaseConfig.connection.host,
'database.connection.port': this.databaseConfig.connection.port || '3306'
'database.connection.user': dbconfig.user,
'database.connection.password': dbconfig.password
},
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.'
});
});
environment: this.system.environment,
help: 'You can run `ghost config` to re-enter the correct credentials. Alternatively you can run `ghost setup` again.'
}));
}

return Promise.reject(error);
});
}

createMySQLUser(ctx) {
createUser(ctx, dbconfig) {
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. 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
// 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);
});
return this._query(`CREATE USER '${username}'@'${dbconfig.host}' IDENTIFIED BY '${randomPassword}';`).then(() => {
this.ui.logVerbose(`MySQL: successfully created new user ${username}`, 'green');

ctx.mysql = {
username: username,
password: randomPassword
};
}).catch((error) => {
// User already exists, run this method again
if (error.errno === 1396) {
this.ui.logVerbose('MySQL: user exists, re-trying user creation with new username', 'yellow');
return this.createUser(ctx, dbconfig);
}

this.ui.logVerbose('MySQL: Unable to create custom Ghost user', 'red');
this.connection.end(); // Ensure we end the connection
return Promise.reject(new cli.errors.SystemError(`Creating new mysql user errored with message: ${error.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);
});
grantPermissions(ctx, dbconfig) {
return this._query(`GRANT ALL PRIVILEGES ON ${dbconfig.database}.* TO '${ctx.mysql.username}'@'${dbconfig.host}';`).then(() => {
this.ui.logVerbose(`MySQL: Successfully granted privileges for user ${ctx.mysql.username}`, 'green');
return this._query('FLUSH PRIVILEGES;');
}).then(() => {
this.ui.logVerbose('MySQL: flushed privileges', 'green');
}).catch((error) => {
this.ui.logVerbose('MySQL: Unable either to grant permissions or flush privileges', 'red');
this.connection.end();
return Promise.reject(new cli.errors.SystemError(`Granting database permissions errored with message: ${error.message}`));
});
}

_query(queryString) {
this.ui.logVerbose(`MySQL: running query > ${queryString}`, 'gray');
return Promise.fromCallback(cb => this.connection.query(queryString, cb));
}
}

Expand Down
5 changes: 4 additions & 1 deletion extensions/mysql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"keywords": [
"ghost-cli-extension"
],
"main": "index.js"
"main": "index.js",
"ghost-cli": {
"before": "ghost-cli-linux"
}
}
6 changes: 3 additions & 3 deletions lib/tasks/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ module.exports = function runMigrations(context) {
'database.connection.host': config.get('database.connection.host')
},
message: 'Invalid database host',
environment: context.environment
environment: context.instance.system.environment
});
} else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
error = new errors.ConfigError({
config: {
'database.connection.user': config.get('database.connection.user'),
'database.connection.password': config.get('database.connection.pasword')
'database.connection.password': config.get('database.connection.password')
},
message: 'Invalid database username or password',
environment: context.environment
environment: context.instance.system.environment
});
}

Expand Down
18 changes: 12 additions & 6 deletions test/unit/tasks/migrate-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,12 @@ describe('Unit: Tasks > Migrate', function () {
}
});

return migrate({ instance: { config: {
get: getStub, set: setStub, save: saveStub
} } }).then(() => {
return migrate({ instance: {
config: {
get: getStub, set: setStub, save: saveStub
},
system: { environment: 'testing' }
} }).then(() => {
expect(false, 'error should have been thrown').to.be.true;
}).catch((error) => {
expect(error).to.be.an.instanceof(errors.ConfigError);
Expand All @@ -155,9 +158,12 @@ describe('Unit: Tasks > Migrate', function () {
}
});

return migrate({ instance: { config: {
get: getStub, set: setStub, save: saveStub
} } }).then(() => {
return migrate({ instance: {
config: {
get: getStub, set: setStub, save: saveStub
},
system: { environment: 'testing' }
} }).then(() => {
expect(false, 'error should have been thrown').to.be.true;
}).catch((error) => {
expect(error).to.be.an.instanceof(errors.ConfigError);
Expand Down

0 comments on commit 860cf18

Please sign in to comment.