diff --git a/lib/plugins/TaskRunner/Drupal.js b/lib/plugins/TaskRunner/Drupal.js index da440af3..d816a1fe 100644 --- a/lib/plugins/TaskRunner/Drupal.js +++ b/lib/plugins/TaskRunner/Drupal.js @@ -14,7 +14,8 @@ class Drupal extends LAMPApp { * @param {object} options - A hash of configuration options specific to this task. * @param {boolean} options.clearCaches - Whether to clear all caches after the build is finished. Defaults to true. * @param {string} options.siteFolder - The site folder to use for this build (the folder within the drupal `sites` folder). Defaults to `default`. - * @param {string} options.database - The name of the database to import if specified. Note that this database *must be added to the assets array separately*. + * @param {string} options.databaseName - The name of the database to use. Defaults to 'drupal'. + * @param {string} options.database - The filename of the database to import if specified. Note that this database *must be added to the assets array separately*. * @param {boolean} options.databaseGzipped - Whether the database was sent gzipped and whether it should therefore be gunzipped before importing. * @param {boolean} options.databaseBzipped - Whether the database was sent bzipped and whether it should therefore be bunzipped before importing. * @param {boolean} options.databaseUpdates - Determines whether to run `drush updb`. @@ -26,6 +27,10 @@ class Drupal extends LAMPApp { * @param {string} options.installArgs - A set of params to concat onto the drush `site-install` command (defaults to ''). * @param {string} options.subDirectory - The directory of the actual web root (defaults to 'docroot'). * @param {string} options.configSyncDirectory - The config sync directory used in Drupal 8. + * @param {string} options.alias - A multisite alias to be used for this site (ex 'example.com'). + * @param {string} options.aliasSubdomain - A multisite alias subdomain to the probo build domain (ex 'example'). Defaults to options.alias. + * @param {string} options.databasePrefix - A string to prefix all database tables. + * @param {boolean} options.skipSetup - A boolean to indicate that we can skip the server setup. You probably shouldn't be setting this yourself, instead it should be passed from the multisite app. * @param {string} [options.settingsAppend] - A snippet to append to the end of the settings.php file. * @param {string} [options.settingsRequireFile] - A file to require at the end of settings.php (in order to get around not * checking settings.php into your repo). @@ -34,10 +39,11 @@ class Drupal extends LAMPApp { constructor(container, options) { super(container, options); - this.databaseName = 'drupal'; + this.databaseName = options.databaseName || 'drupal'; this.options.siteFolder = options.siteFolder || 'default'; this.options.profileName = options.profileName || 'standard'; + // clearCaches must be set to explicitly false this.options.clearCaches = (options.clearCaches || typeof options.clearCaches === 'undefined'); this.options.drupalVersion = options.drupalVersion || constants.DEFAULT_DRUPAL_VERSION; @@ -70,12 +76,14 @@ class Drupal extends LAMPApp { * */ populateScriptArray() { - this.addScriptSetup(); - if (this.options.makeFile) { - this.addScriptRunMakeFile(); - } - else { - this.addScriptSymlinks(); + if (!this.options.skipSetup) { + this.addScriptSetup(); + if (this.options.makeFile) { + this.addScriptRunMakeFile(); + } + else { + this.addScriptSymlinks(); + } } this.addScriptCreateDatbase(); this.addScriptAppendSettingsPHPSettings(); @@ -98,8 +106,9 @@ class Drupal extends LAMPApp { if (this.options.clearCaches) { this.addScriptClearCaches(); } - - this.addScriptApachePhp(); + if (!this.options.skipSetup) { + this.addScriptApachePhp(); + } } addScriptAppendSettingsPHPSettings() { @@ -118,7 +127,7 @@ class Drupal extends LAMPApp { '\\$databases = array(', ' \'default\' => array(', ' \'default\' => array(', - ' \'database\' => \'drupal\',', + ` \'database\' => \'${this.databaseName}\',`, ' \'username\' => \'root\',', ' \'password\' => \'strongpassword\',', ' \'host\' => \'localhost\',', @@ -145,7 +154,7 @@ class Drupal extends LAMPApp { `\\$databases = array(`, ` 'default' => array(`, ` 'default' => array(`, - ` 'database' => 'drupal',`, + ` 'database' => '${this.databaseName}',`, ` 'username' => 'root',`, ` 'password' => 'strongpassword',`, ` 'host' => 'localhost',`, @@ -169,12 +178,35 @@ class Drupal extends LAMPApp { } appendCustomSettings() { + var settings_path = `/var/www/html/sites/${this.options.siteFolder}/settings.php`; + + if (this.options.alias) { + var alias; + var subdomain; + var build_protocol; + var build_domain; + var fqdn; + + alias = this.options.alias; + subdomain = this.options.aliasSubdomain || alias; + build_protocol = this.container.build.links.build.split('://')[0]; + build_domain = this.container.build.links.build.split('://')[1]; + fqdn = build_protocol + '://' + subdomain + '.' + build_domain; + + this.script.push(`echo "\$sites[${fqdn}] = ${alias};" >> ${settings_path}`); + } + + if (this.options.databasePrefix) { + this.script.push(`echo "\$db_prefix = '${this.options.databasePrefix}';" >> ${settings_path}`); + } + if (this.options.settingsRequireFile) { - let command = 'echo "require_once(\'' + this.options.settingsRequireFile + '\');" >> /var/www/html/sites/' + this.options.siteFolder + '/settings.php'; + let command = `echo "require_once(\'' + this.options.settingsRequireFile + '\');" >> ${settings_path}`; this.script.push(command); } + if (this.options.settingsAppend) { - let command = 'echo ' + shellEscape([this.options.settingsAppend]) + ' >> /var/www/html/sites/' + this.options.siteFolder + '/settings.php'; + let command = 'echo ' + shellEscape([this.options.settingsAppend]) + ` >> ${settings_path}`; this.script.push(command); } } diff --git a/lib/plugins/TaskRunner/DrupalMultisite.js b/lib/plugins/TaskRunner/DrupalMultisite.js new file mode 100644 index 00000000..20fb8ffb --- /dev/null +++ b/lib/plugins/TaskRunner/DrupalMultisite.js @@ -0,0 +1,77 @@ +'use strict'; +var shellEscape = require('shell-escape'); +var crypto = require('crypto'); + +var constants = require('./constants'); + +var Drupal = require('./Drupal'); +var LAMPApp = require('./LAMPApp'); + +class DrupalMultisite extends LAMPApp { + + /** + * Options (used by this task in addition to the Drupal options): + * @param {array} options.sites - A hash of sites. Each site supports the same options as the Drupal plugin, except for @TODO. + * @augments Drupal + */ + constructor(container, options) { + super(container, options); + + this.script = []; + this.populateScriptArray(); + this.setScript(this.script); + } + + description() { + return `${this.plugin} 'Provisioning Drupal Multisite!'`; + } + + populateScriptArray() { + this.addScriptSetup(); + if (this.options.makeFile) { + this.addScriptRunMakeFile(); + } + else { + this.addScriptSymlinks(); + } + + /** + * Loop through each site, merging the plugin options with the site options. + * Virtualize the script output from a Drupal app (skipping the setup), and + * add that to our script. Track which databases have already been imported + * and only import each one once. + */ + for (var site in this.options.sites) { + if (this.options.sites.hasOwnProperty(site)) { + var drupal; + var importedDatabases = []; + var site_options = this.options.sites[site]; + site_options.siteFolder = this.options.siteFolder || site; + site_options.alias = site; + site_options.virtualize = true; + site_options.skipSetup = true; + site_options = Object.assign({}, this.options, site_options); + + if (site_options.database) { + if (importedDatabases.indexOf(site_options.database) != -1) { + site_options.skipDatabase = true; + } + importedDatabases.push(site_options.database); + } + + drupal = new Drupal(this.container, site_options); + this.script = this.script.concat(drupal.script); + } + } + this.addScriptApachePhp(); + } + + // @TODO - this is going to cause problems someday. We should make a Drupal-like + // Plugin that extends LAMPApp, and then Drupal and DrupalMultisite can extend that. + addScriptRunMakeFile() { + this.script.push('cd $SRC_DIR ; drush make ' + this.options.makeFile + ' /var/www/html --force-complete'); + this.script.push('rsync -a $SRC_DIR/ /var/www/html/profiles/' + this.options.profileName); + } +} + +module.exports = DrupalMultisite; diff --git a/lib/plugins/TaskRunner/Script.js b/lib/plugins/TaskRunner/Script.js index fd42207d..2fe9130d 100644 --- a/lib/plugins/TaskRunner/Script.js +++ b/lib/plugins/TaskRunner/Script.js @@ -6,9 +6,12 @@ var combine = require('stream-combiner'); module.exports = class Script extends require('./AbstractPlugin') { - // requires options: - // - script: string to pipe to the container on stdin or an array of strings - // - secrets: Array of secret strings that need to be filtered out of output + /** + * Options (used by this task): + * @param {object} options.script - String to pipe to the container on stdin or an array of strings. + * @param {array} options.secrets - Array of secret strings that need to be filtered out of output. + * @param {boolean} options.virtualize - If set to true, will build the script but will not run it. + */ constructor(container, options) { super(container, options); options.tty = false; @@ -16,9 +19,11 @@ module.exports = class Script extends require('./AbstractPlugin') { this.setScript(options.script || ''); var self = this; - this.on('running', function() { - self.runScript(); - }); + if (!options.virtualize) { + this.on('running', function() { + self.runScript(); + }); + } } setScript(script) { diff --git a/test/tasks/DrupalApp.js b/test/tasks/DrupalApp.js index 5dd192aa..0c4ce992 100644 --- a/test/tasks/DrupalApp.js +++ b/test/tasks/DrupalApp.js @@ -22,6 +22,10 @@ describe('Drupal App', function() { database: 'my-cool-db.sql', databaseGzipped: true, clearCaches: false, + databaseName: 'cooldb', + databasePrefix: 'prefix', + alias: 'site1.com', + aliasSubdomain: 'siteone', }; var app2 = new DrupalApp(mockContainer, options2); @@ -34,6 +38,7 @@ describe('Drupal App', function() { app.script.should.containEql('ln -s $SRC_DIR /var/www/html'); app.script.should.containEql('mysql -e \'create database drupal\''); + app2.script.should.containEql('mysql -e \'create database cooldb\''); app.script.should.containEql( 'cat $ASSET_DIR/my-cool-db.sql | $(mysql -u root --password=strongpassword drupal)' @@ -47,6 +52,7 @@ describe('Drupal App', function() { it('cats the settings.php file', function() { app.script.should.containEql('\'database\' => \'drupal\''); + app2.script.should.containEql('\'database\' => \'cooldb\''); app.script.should.containEql('\'username\' => \'root\''); app.script.should.containEql('\'password\' => \'strongpassword\''); }); @@ -55,4 +61,9 @@ describe('Drupal App', function() { app.script.should.containEql('drush --root=/var/www/html cache-clear all'); app2.script.should.not.containEql('drush --root=/var/www/html cache-clear all'); }); + + it('handles multisite configuration', function() { + app2.script.should.containEql("$sites[http://siteone.abc123.probo.build] = site1.com;"); + app2.script.should.containEql("$db_prefix = 'prefix';"); + }); }); diff --git a/test/tasks/DrupalMultisiteApp.js b/test/tasks/DrupalMultisiteApp.js new file mode 100644 index 00000000..fbd12f4b --- /dev/null +++ b/test/tasks/DrupalMultisiteApp.js @@ -0,0 +1,84 @@ +'use strict'; +var DrupalMultisiteApp = require('../../lib/plugins/TaskRunner/DrupalMultisite'); + +var mockContainer = { + log: {child: function() {}}, + build: { + links: { + build: 'http://abc123.probo.build', + }, + }, +}; + + +describe('Multisite Drupal App', function() { + + // Each site has its own database. + var options = { + sites: { + 'site1.com': { + database: 'my-cool-db.sql', + databaseName: 'cooldb', + aliasSubdomain: 'site1', + siteFolder: 'site1folder', + }, + 'site2.com': { + database: 'my-lame-db.sql', + databaseName: 'lamedb', + aliasSubdomain: 'sitetwo', + }, + }, + phpConstants: { + multisite: 2, + }, + + }; + var app = new DrupalMultisiteApp(mockContainer, options); + + // Each site has a shared database with prefixes. + var options2 = { + sites: { + 'site1.com': { + databasePrefix: 'cooldb_prefix', + aliasSubdomain: 'site1', + }, + 'site2.com': { + databasePrefix: 'lamedb_prefix', + aliasSubdomain: 'sitetwo', + }, + }, + database: 'my-cool-db.sql', + databaseName: 'cooldb', + }; + var app2 = new DrupalMultisiteApp(mockContainer, options2); + + it('adds setup commands only once', function() { + (app.script.match(/ln \-s/g) || []).length.should.eql(2); + (app.script.match(/proboPhpConstants/g) || []).length.should.eql(2); + }); + + it('handles multisite with multiple databases', function() { + app.script.should.containEql('mysql -e \'create database cooldb\''); + app.script.should.containEql('mysql -e \'create database lamedb\''); + app.script.should.containEql('\'database\' => \'cooldb\''); + app.script.should.containEql('\'database\' => \'lamedb\''); + app.script.should.containEql("$sites[http://site1.abc123.probo.build] = site1.com;"); + app.script.should.containEql("$sites[http://sitetwo.abc123.probo.build] = site2.com;"); + (app.script.match(/lamedb/g) || []).length.should.eql(4); + (app.script.match(/cooldb/g) || []).length.should.eql(4); + }); + + it('handles multisite with a single shared database', function() { + app2.script.should.containEql('mysql -e \'create database cooldb\''); + app2.script.should.not.containEql('mysql -e \'create database lamedb\''); + app2.script.should.containEql("$db_prefix = 'cooldb_prefix';"); + app2.script.should.containEql("$db_prefix = 'lamedb_prefix';"); + + // Only import the db once + (app.script.match(/my-cool-db\.sql/g) || []).length.should.eql(2); + }); + + it('should not use the default site folder in multisite repos', function() { + app.script.should.not.containEql("var/www/html/sites/default/settings.php"); + }); +});