@wordpress/env
Version:
A zero-config, self contained local WordPress environment for development and testing.
346 lines (322 loc) • 9.97 kB
JavaScript
;
/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
/**
* Internal dependencies
*/
const { hasSameCoreSource } = require( './wordpress' );
const { dbEnv } = require( '../../config' );
const getHostUser = require( './get-host-user' );
/**
* @typedef {import('../../config').WPConfig} WPConfig
* @typedef {import('../../config').WPEnvironmentConfig} WPEnvironmentConfig
*/
/**
* Gets the volume mounts for an individual service.
*
* @param {string} workDirectoryPath The working directory for wp-env.
* @param {WPEnvironmentConfig} config The service config to get the mounts from.
* @param {string} hostUsername The username of the host running wp-env.
* @param {string} wordpressDefault The default internal path for the WordPress
* source code (such as tests-wordpress).
*
* @return {string[]} An array of volumes to mount in string format.
*/
function getMounts(
workDirectoryPath,
config,
hostUsername,
wordpressDefault = 'wordpress'
) {
// Top-level WordPress directory mounts (like wp-content/themes)
const directoryMounts = Object.entries( config.mappings ).map(
( [ wpDir, source ] ) => `${ source.path }:/var/www/html/${ wpDir }`
);
const pluginMounts = config.pluginSources.map(
( source ) =>
`${ source.path }:/var/www/html/wp-content/plugins/${ source.basename }`
);
const themeMounts = config.themeSources.map(
( source ) =>
`${ source.path }:/var/www/html/wp-content/themes/${ source.basename }`
);
const userHomeMount =
wordpressDefault === 'wordpress'
? `user-home:/home/${ hostUsername }`
: `tests-user-home:/home/${ hostUsername }`;
const corePHPUnitMount = `${ path.join(
workDirectoryPath,
wordpressDefault === 'wordpress'
? 'WordPress-PHPUnit'
: 'tests-WordPress-PHPUnit',
'tests',
'phpunit'
) }:/wordpress-phpunit`;
const coreMount = `${
config.coreSource ? config.coreSource.path : wordpressDefault
}:/var/www/html`;
return [
...new Set( [
coreMount, // Must be first because of some operations later that expect it to be!
corePHPUnitMount,
userHomeMount,
...directoryMounts,
...pluginMounts,
...themeMounts,
] ),
];
}
/**
* Creates a docker-compose config object which, when serialized into a
* docker-compose.yml file, tells docker-compose how to run the environment.
*
* @param {WPConfig} config A wp-env config object.
*
* @return {Object} A docker-compose config object, ready to serialize into YAML.
*/
module.exports = function buildDockerComposeConfig( config ) {
const testsEnabled = config.testsEnvironment !== false;
// Since we are mounting files from the host operating system
// we want to create the host user in some of our containers.
// This ensures ownership parity and lets us access files
// and folders between the containers and the host.
const hostUser = getHostUser();
const developmentMounts = getMounts(
config.workDirectoryPath,
config.env.development,
hostUser.name
);
const testsMounts = testsEnabled
? getMounts(
config.workDirectoryPath,
config.env.tests,
hostUser.name,
'tests-wordpress'
)
: [];
// We use a custom Dockerfile in order to make sure that
// the current host user exists inside the container.
const imageBuildArgs = {
HOST_USERNAME: hostUser.name,
HOST_UID: hostUser.uid,
HOST_GID: hostUser.gid,
};
// When both tests and development reference the same WP source, we need to
// ensure that tests pulls from a copy of the files so that it maintains
// a separate DB and config. Additionally, if the source type is local we
// need to ensure:
//
// 1. That changes the user makes within the "core" directory are
// served in both the development and tests environments.
// 2. That the development and tests environment use separate
// databases and `wp-content/uploads`.
//
// To do this we copy the local "core" files ($wordpress) to a tests
// directory ($tests-wordpress) and instruct the tests environment
// to source its files like so:
//
// - wp-config.php <- $tests-wordpress/wp-config.php
// - wp-config-sample.php <- $tests-wordpress/wp-config.php
// - wp-content <- $tests-wordpress/wp-content
// - * <- $wordpress/*
//
// https://github.com/WordPress/gutenberg/issues/21164
if (
testsEnabled &&
config.env.development.coreSource &&
hasSameCoreSource( [ config.env.development, config.env.tests ] )
) {
const wpSource = config.env.development.coreSource;
testsMounts.shift(); // Remove normal core mount.
testsMounts.unshift(
...[
`${ wpSource.testsPath }:/var/www/html`,
...( wpSource.type === 'local'
? fs
.readdirSync( wpSource.path )
.filter(
( filename ) =>
filename !== 'wp-config.php' &&
filename !== 'wp-config-sample.php' &&
filename !== 'wp-content'
)
.map(
( filename ) =>
`${ path.join(
wpSource.path,
filename
) }:/var/www/html/${ filename }`
)
: [] ),
]
);
}
// Set the default ports based on the config values.
const developmentPorts = `\${WP_ENV_PORT:-${ config.env.development.port }}:80`;
const developmentMysqlPorts = `\${WP_ENV_MYSQL_PORT:-${
config.env.development.mysqlPort ?? ''
}}:3306`;
const developmentPhpmyadminPorts = `\${WP_ENV_PHPMYADMIN_PORT:-${
config.env.development.phpmyadminPort ?? ''
}}:80`;
// MySQL healthcheck using MariaDB's official healthcheck.sh script.
// --connect: verifies TCP connection and that entrypoint has finished
// --innodb_initialized: ensures InnoDB storage engine is fully initialized
// MARIADB_AUTO_UPGRADE env var ensures healthcheck user exists for existing installations.
// Timing is generous to support slow CI environments.
const mysqlHealthcheck = {
test: [ 'CMD', 'healthcheck.sh', '--connect', '--innodb_initialized' ],
interval: '5s',
timeout: '10s',
retries: 12,
start_period: '60s',
};
// Build the services object, conditionally including tests services.
const services = {
mysql: {
image: 'mariadb:lts',
ports: [ developmentMysqlPorts ],
environment: {
MYSQL_ROOT_HOST: '%',
MYSQL_ROOT_PASSWORD: dbEnv.credentials.WORDPRESS_DB_PASSWORD,
MYSQL_DATABASE: dbEnv.development.WORDPRESS_DB_NAME,
// Ensures healthcheck user is created for existing installations.
MARIADB_AUTO_UPGRADE: '1',
},
volumes: [ 'mysql:/var/lib/mysql' ],
healthcheck: mysqlHealthcheck,
},
wordpress: {
depends_on: {
mysql: {
condition: 'service_healthy',
},
},
build: {
context: '.',
dockerfile: 'WordPress.Dockerfile',
args: imageBuildArgs,
},
ports: [ developmentPorts ],
environment: {
APACHE_RUN_USER: '#' + hostUser.uid,
APACHE_RUN_GROUP: '#' + hostUser.gid,
...dbEnv.credentials,
...dbEnv.development,
WP_TESTS_DIR: '/wordpress-phpunit',
},
volumes: developmentMounts,
extra_hosts: [ 'host.docker.internal:host-gateway' ],
},
cli: {
depends_on: [ 'wordpress' ],
build: {
context: '.',
dockerfile: 'CLI.Dockerfile',
args: imageBuildArgs,
},
volumes: developmentMounts,
user: hostUser.fullUser,
environment: {
...dbEnv.credentials,
...dbEnv.development,
WP_TESTS_DIR: '/wordpress-phpunit',
},
extra_hosts: [ 'host.docker.internal:host-gateway' ],
},
phpmyadmin: {
image: 'phpmyadmin',
ports: [ developmentPhpmyadminPorts ],
environment: {
PMA_PORT: 3306,
PMA_HOST: 'mysql',
PMA_USER: 'root',
PMA_PASSWORD: 'password',
},
},
};
const volumes = {
...( ! config.env.development.coreSource && { wordpress: {} } ),
mysql: {},
'user-home': {},
};
if ( testsEnabled ) {
const testsPorts = `\${WP_ENV_TESTS_PORT:-${ config.env.tests.port }}:80`;
const testsMysqlPorts = `\${WP_ENV_TESTS_MYSQL_PORT:-${
config.env.tests.mysqlPort ?? ''
}}:3306`;
const testsPhpmyadminPorts = `\${WP_ENV_TESTS_PHPMYADMIN_PORT:-${
config.env.tests.phpmyadminPort ?? ''
}}:80`;
services[ 'tests-mysql' ] = {
image: 'mariadb:lts',
ports: [ testsMysqlPorts ],
environment: {
MYSQL_ROOT_HOST: '%',
MYSQL_ROOT_PASSWORD: dbEnv.credentials.WORDPRESS_DB_PASSWORD,
MYSQL_DATABASE: dbEnv.tests.WORDPRESS_DB_NAME,
// Ensures healthcheck user is created for existing installations.
MARIADB_AUTO_UPGRADE: '1',
},
volumes: [ 'mysql-test:/var/lib/mysql' ],
healthcheck: mysqlHealthcheck,
};
services[ 'tests-wordpress' ] = {
depends_on: {
'tests-mysql': {
condition: 'service_healthy',
},
},
build: {
context: '.',
dockerfile: 'Tests-WordPress.Dockerfile',
args: imageBuildArgs,
},
ports: [ testsPorts ],
environment: {
APACHE_RUN_USER: '#' + hostUser.uid,
APACHE_RUN_GROUP: '#' + hostUser.gid,
...dbEnv.credentials,
...dbEnv.tests,
WP_TESTS_DIR: '/wordpress-phpunit',
},
volumes: testsMounts,
extra_hosts: [ 'host.docker.internal:host-gateway' ],
};
services[ 'tests-cli' ] = {
depends_on: [ 'tests-wordpress' ],
build: {
context: '.',
dockerfile: 'Tests-CLI.Dockerfile',
args: imageBuildArgs,
},
volumes: testsMounts,
user: hostUser.fullUser,
environment: {
...dbEnv.credentials,
...dbEnv.tests,
WP_TESTS_DIR: '/wordpress-phpunit',
},
extra_hosts: [ 'host.docker.internal:host-gateway' ],
};
services[ 'tests-phpmyadmin' ] = {
image: 'phpmyadmin',
ports: [ testsPhpmyadminPorts ],
environment: {
PMA_PORT: 3306,
PMA_HOST: 'tests-mysql',
PMA_USER: 'root',
PMA_PASSWORD: 'password',
},
};
if ( ! config.env.tests.coreSource ) {
volumes[ 'tests-wordpress' ] = {};
}
volumes[ 'mysql-test' ] = {};
volumes[ 'tests-user-home' ] = {};
}
return { services, volumes };
};