@lando/acquia
Version:
A Lando plugin that provides a tight integration with Acquia.
343 lines (321 loc) • 11.9 kB
JavaScript
'use strict';
/**
* @file
* This file defines the base Lando builder for Acquia recipes.
* It provides common configurations for services (appserver, database),
* tooling (Composer, Drush, DB CLIs), and default file handling necessary
* to emulate an Acquia environment locally.
*/
// Modules
const fs = require('fs');
const _ = require('lodash');
const path = require('path');
const semver = require('semver');
const utils = require('../lib/utils.js');
const warnings = require('../lib/warnings.js');
// "Constants"
/** Default Drush 8 version to use. */
const DRUSH8 = '8.4.10';
/** Default Drush 7 version to use (typically for older PHP versions). */
const DRUSH7 = '7.4.0';
/**
* Default tooling definitions for Acquia recipes.
* Includes configurations for Composer, database import/export, and PHP CLI.
* @type {object}
*/
const toolingDefaults = {
'composer': {
service: 'appserver',
cmd: 'composer --ansi',
},
'db-import <file>': {
service: ':host',
description: 'Imports a dump file into a database service',
cmd: '/helpers/sql-import.sh',
user: 'root',
options: {
'host': {
description: 'The database service to use',
default: 'database',
alias: ['h'],
},
'no-wipe': {
description: 'Do not destroy the existing database before an import',
boolean: true,
},
},
},
'db-export [file]': {
service: ':host',
description: 'Exports database from a database service to a file',
cmd: '/helpers/sql-export.sh',
user: 'root',
options: {
host: {
description: 'The database service to use',
default: 'database',
alias: ['h'],
},
stdout: {
description: 'Dump database to stdout',
},
},
},
'php': {
service: 'appserver',
cmd: 'php',
},
};
/**
* Configuration for the MySQL command-line interface.
* @type {object}
*/
const mysqlCli = {
service: ':host',
description: 'Drops into a MySQL shell on a database service',
cmd: 'mysql -uroot',
options: {
host: {
description: 'The database service to use',
default: 'database',
alias: ['h'],
},
},
};
/**
* Configuration for the PostgreSQL (psql) command-line interface.
* @type {object}
*/
const postgresCli = {
service: ':host',
description: 'Drops into a psql shell on a database service',
cmd: 'psql -Upostgres',
user: 'root',
options: {
host: {
description: 'The database service to use',
default: 'database',
alias: ['h'],
},
},
};
/**
* Determines the database type (e.g., 'mysql', 'postgres') based on Lando app configuration.
*
* @param {object} options The Lando recipe/service options.
* @return {string} The database type string (e.g., 'mysql:8.0', 'postgres:13', defaults to 'mysql').
*/
const getDatabaseType = options => {
return _.get(options, '_app.config.services.database.type', options.database) ?? 'mysql';
};
/**
* Determines and verifies default configuration files (e.g., for vhosts, database) based on options.
* Modifies `options.defaultFiles` in place if a Nginx vhost or specific MySQL versions are used,
* and removes entries for files that don't exist at `options.confDest`.
*
* @param {object} options The Lando recipe options, including `via`, `database`, `defaultFiles`, and `confDest`.
* @return {object} The modified `options.defaultFiles` object.
*/
const getConfigDefaults = options => {
// Get the viaconf
if (_.startsWith(options.via, 'nginx')) options.defaultFiles.vhosts = 'default.conf.tpl';
// Get the default db conf
const dbConfig = getDatabaseType(options);
const database = _.first(dbConfig.split(':'));
const version = _.last(dbConfig.split(':')).substring(0, 2);
if (database.includes('mysql')) {
if (version === '8.') {
options.defaultFiles.database = 'mysql8.cnf';
} else {
options.defaultFiles.database = 'mysql.cnf';
}
}
// Verify files exist and remove if it doesn't
_.forEach(options.defaultFiles, (file, type) => {
if (!fs.existsSync(`${options.confDest}/${file}`)) {
delete options.defaultFiles[type];
}
});
// Return
return options.defaultFiles;
};
/**
* Constructs the service definitions for the appserver and database based on recipe options.
*
* @param {object} options The Lando recipe options (e.g., `php` version, `database` type, `webroot`).
* @return {object} An object containing definitions for `appserver` and `database` services.
*/
const getServices = options => ({
appserver: {
build_as_root_internal: options.build_root,
build_internal: options.build,
composer: options.composer,
composer_version: options.composer_version,
config: getServiceConfig(options),
run_as_root_internal: options.run_root,
ssl: true,
type: `acquia-php:${options.php}`,
via: options.via,
xdebug: options.xdebug,
webroot: options.webroot,
},
database: {
config: getServiceConfig(options, ['database']),
authentication: 'mysql_native_password',
type: `acquia-${options.database}`,
portforward: true,
creds: {
user: options.recipe,
password: options.recipe,
database: options.recipe,
},
},
});
/**
* Provides the appropriate database CLI tooling configuration based on the database type.
*
* @param {string} database The database type string (e.g., 'mysql', 'postgres', 'mongo').
* @return {object | undefined} An object containing the tooling definition for the specified database (e.g., `{mysql: mysqlCli}`), or undefined if not supported.
*/
const getDbTooling = database => {
// Make sure we strip out any version number
database = database.split(':')[0];
// Choose wisely
if (_.includes(['mysql'], database)) {
return {mysql: mysqlCli};
} else if (database === 'postgres') {
return {psql: postgresCli};
} else if (database === 'mongo') {
return {mongo: {
service: 'database',
description: 'Drop into the mongo shell',
}};
}
};
/**
* Gathers service-specific configuration file paths.
* It checks for user-provided paths in `options.config` first, then falls back to default files
* specified in `options.defaultFiles` located in `options.confDest`.
*
* @param {object} options The Lando recipe options.
* @param {Array<string>} [types] The types of configuration to look for (e.g., 'php' for php.ini).
* @return {object} An object mapping configuration types to their resolved file paths.
*/
const getServiceConfig = (options, types = ['php', 'server', 'vhosts']) => {
const config = {};
_.forEach(types, type => {
if (_.has(options, `config.${type}`)) {
config[type] = options.config[type];
} else if (!_.has(options, `config.${type}`) && _.has(options, `defaultFiles.${type}`)) {
if (_.has(options, 'confDest')) {
config[type] = path.join(options.confDest, options.defaultFiles[type]);
}
}
});
return config;
};
/**
* Merges default tooling with database-specific CLI tooling.
*
* @param {object} options The Lando recipe options, used to determine `options.database`.
* @return {object} The complete tooling configuration object.
*/
const getTooling = options => _.merge({}, toolingDefaults, getDbTooling(options.database));
/*
* Build Acquia base recipe
*/
/**
* Lando builder definition for the Acquia base recipe.
* This is intended to be extended by more specific Acquia recipe builders.
* It sets up default configurations for PHP, database, web server, and common tooling like Drush and Composer.
*/
module.exports = {
name: '_acquia-base',
parent: '_recipe',
/** Default configuration for the Acquia base recipe. */
config: {
/** @type {Array<string>} Array of shell commands to run during the build phase of the appserver. */
build: [],
/** @type {object} Composer requirements. Keys are package names, values are version constraints. */
composer: {},
/** @type {string} Source directory for default configuration files (e.g., php.ini, vhost templates). */
confSrc: path.resolve(__dirname, '..', 'config'),
/** @type {object} User-overrideable configuration file paths for services. */
config: {},
/** @type {string} Default database type and version (e.g., 'mysql:5.7', 'postgres:12'). */
database: 'mysql',
/** @type {object} Mapping of default configuration file names for different components. */
defaultFiles: {
php: 'php.ini',
},
/** @type {string} Default PHP version. */
php: '7.2',
/** @type {object} Default Drush tooling configuration. */
tooling: {drush: {
service: 'appserver',
}},
/** @type {string} Web server type ('apache' or 'nginx'). */
via: 'apache',
/** @type {string} Path to the webroot within the appserver. */
webroot: '.',
/** @type {boolean} Whether to enable Xdebug. */
xdebug: false,
/** @type {object} Proxy configuration. */
proxy: {},
},
/**
* The builder function that returns the LandoAcquiaBase class.
* @param {Function} parent The parent class this builder extends (_recipe).
* @param {object} config The default configuration for this recipe.
* @return {Function} The LandoAcquiaBase class.
*/
builder: (parent, config) => class LandoAcquiaBase extends parent {
/**
* Constructor for the LandoAcquiaBase class.
* Merges default and user-provided options, configures Drush installation (Composer or Phar),
* sets up warnings for modern Drush versions, and defines legacy environment variables.
*
* @param {string} id The application instance ID.
* @param {object} [options] User-provided options to override defaults.
*/
constructor(id, options = {}) {
options = _.merge({}, config, options);
// Set the default drush version if we don't have it
if (!_.has(options, 'drush')) options.drush = (options.php === '5.3') ? DRUSH7 : DRUSH8;
// Figure out the drush situation
if (options.drush !== false) {
// Start by assuming a composer based install
options.composer['drush/drush'] = options.drush;
// Switch to phar based install if we can
if (semver.valid(options.drush) && semver.major(options.drush) === 8) {
delete options.composer['drush/drush'];
options.build.unshift(utils.getDrush(options.drush, ['drush', '--version']));
}
// Attempt to set a warning if possible
const coercedDrushVersion = semver.valid(semver.coerce(options.drush));
if (!_.isNull(coercedDrushVersion) && semver.gte(coercedDrushVersion, '10.0.0')) {
options._app.addWarning(warnings.drushWarn(options.drush));
}
}
// Set legacy envars
options.services = _.merge({}, options.services, {appserver: {overrides: {
environment: {
SIMPLETEST_BASE_URL: (options.via === 'nginx') ? 'https://appserver_nginx' : 'https://appserver',
SIMPLETEST_DB: `mysql://${options.recipe}:${options.recipe}@database/${options.recipe}`,
},
}}});
// Rebase on top of any default config we might already have
options.defaultFiles = _.merge({}, getConfigDefaults(_.cloneDeep(options)), options.defaultFiles);
options.services = _.merge({}, getServices(options), options.services);
options.tooling = _.merge({}, getTooling(options), options.tooling);
// Switch the proxy if needed
if (!_.has(options, 'proxyService')) {
if (_.startsWith(options.via, 'nginx')) options.proxyService = 'appserver_nginx';
else if (_.startsWith(options.via, 'apache')) options.proxyService = 'appserver';
}
options.proxy = _.set(options.proxy, options.proxyService, [`${options.app}.${options._app._config.domain}`]);
// Send downstream
super(id, _.merge({}, config, options));
}
},
};