Newer
Older
use LakeDrops\Component\Composer\BaseHandler;
use LakeDrops\DockerTraefik\Traefik;
use LakeDrops\DrupalEnvironment\Handler as DrupalEnvironment;

jurgenhaas
committed
use Symfony\Component\Yaml\Yaml;
/**
* Class Handler.
*
* @package LakeDrops\Docker4Drupal
*/
class Handler extends BaseHandler {
public function configId(): string {
return 'docker4drupal';
}
/**
* {@inheritdoc}
*/
protected function configDefault(): array {
$projectname = getenv('COMPOSE_PROJECT_NAME');
if (empty($projectname)) {$projectname = str_replace([' ', '-', '_', '.'], '', basename(getcwd()));
$this->env->put('COMPOSE_PROJECT_NAME', $projectname);
}
$options = [
'projectname' => $projectname,
'ci_home' => '/home/gitlab-runner',
'docker0' => [
'ip' => ($this->isCiContext() || $this->isLocalDevMode()) ?
$this->getDockerGateway() :
$this->getLocalIpv4('docker0'),
'proxy' => ($this->isCiContext() || $this->isLocalDevMode()) ?
$this->getDockerProxy($projectname) :
FALSE,
'traefik' => [
'domain' => $this->env->receive('traefik_domain', '', 'docker.localhost'),
'usessl' => $this->env->receive('traefik_usessl', '', '0'),
'port' => $this->env->receive('traefik_port', '', '8000'),
'ports' => $this->env->receive('traefik_ports', '', '8443'),
'cert' => $this->env->receive('traefik_cert', '', 'fullchain.pem'),
'key' => $this->env->receive('traefik_key', '', 'privkey.pem'),
'portainer' => $this->env->receive('traefik_portainer', '', '0'),
'live' => [
'root' => '',
'uri' => '',
'host' => '',
'user' => $this->env->receive('live_host_username', 'Remote username for host of the live site', getenv('USER')),
],
'drush' => [
'sql' => [
'tables' => [
'structure' => [
'cache',
'cachetags',
'cache_*',
'history',
'search_*',
'sessions',
'watchdog',
],
'skip' => [
'migration_*',
],
],
],
],
'drupal' => [
'version' => $this->env->receiveGlobal('PHP_VERSION', 'PHP version', '7.4'),
'xdebug' => $this->env->receiveGlobal('PHP_DEBUG', 'PHP debug', '0'),
'coverage' => $this->env->receiveGlobal('PHP_COVERAGE', 'PHP coverage', '0'),

jurgenhaas
committed
'dbserver' => [
'type' => 'mariadb',

jurgenhaas
committed
],
'webserver' => [
'type' => 'apache',
'overwriteconfig' => FALSE,
],

jurgenhaas
committed
'mailhog' => [

jurgenhaas
committed
'host' => $this->env->receiveGlobal('MAILHOG_HOST', 'MailHog Host', 'smtp.freesmtpservers.com'),
'port' => $this->env->receiveGlobal('MAILHOG_PORT', 'MailHog Port', '25'),

jurgenhaas
committed
'username' => $this->env->receiveGlobal('MAILHOG_USERNAME', 'MailHog Username'),
'password' => $this->env->receiveGlobal('MAILHOG_PASSWORD', 'MailHog Password'),

jurgenhaas
committed
'mechanism' => $this->env->receiveGlobal('MAILHOG_MECHANISM', 'MailHog Auth Mechanism', 'NONE'),

jurgenhaas
committed
],
'varnish' => [
'enable' => 0,
],
'redis' => [
],
'dbbrowser' => [
'type' => 'pma',
],
'solr' => [
'enable' => 0,
'version' => '4.8.0',
],
'node' => [
'enable' => 0,
'key' => '',
'path' => '',
],
'memcached' => [
'enable' => 0,
],
'rsyslog' => [
'enable' => 0,
],
'athenapdf' => [
'enable' => 0,
'key' => '',
],
'blackfire' => [
'enable' => 0,
'id' => '',
'token' => '',
],
'webgrind' => [
'enable' => 0,
],
'selenium' => [
'enable' => 0,
],
'elasticsearch' => [
'enable' => 0,
],
'wkhtmltox' => [
'enable' => 0,
],
];
if ($this->isCiContext() || $this->isLocalDevMode()) {
$projectRoot = $this->getDockerMountSource(getenv('CI_PROJECT_DIR'));
}
else {
$projectRoot = getcwd();
}
// Check if SSH auth sockets are supported.
$ssh_auth_sock = getenv('SSH_AUTH_SOCK');
$options['php']['ssh'] = !empty($ssh_auth_sock);
if ($options['php']['ssh']) {
$options['php']['ssh_auth_sock'] = ($this->isCiContext() || $this->isLocalDevMode()) ?
$this->getDockerMountSource('/ssh-agent') :
'$SSH_AUTH_SOCK';
}
$options['projectroot'] = $projectRoot;
$options['projectdomain'] = $options['projectname'] . '.' . $options['traefik']['domain'];
$options['projectprotocol'] = 'http' . ($options['traefik']['usessl'] ? 's' : '');
$options['projectport'] = '';
if ($options['traefik']['usessl'] && (int) $options['traefik']['ports'] !== 443) {
$options['projectport'] = ':' . $options['traefik']['ports'];
}
else if (!$options['traefik']['usessl'] && (int) $options['traefik']['port'] !== 80) {
$options['projectport'] = ':' . $options['traefik']['port'];
}
return $options;
}
/**
* {@inheritdoc}
*/
protected function postInit(): void {
$this->env->put('PHP_VERSION', $this->config->readValue(['php', 'version']), TRUE);
* @param bool $overwrite

jurgenhaas
committed
public function configureProject(bool $overwrite = FALSE): void {
$this->init();
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Configure Drupal environment if avaiable.
if ($this->getPackage('lakedrops/drupal-environment')) {
$handler = new DrupalEnvironment($this->composer, $this->io);
$handler->setupLakeDropsProject();
// Update config for production build.
if (getenv('LAKEDROPS_BUILD_NG') === 'yes') {
$config = $handler->getConfig();
$root = '/drupal/' . getenv('CI_PROJECT_ID') . '/' . getenv('CI_COMMIT_BRANCH');
$drupal = $this->config->readValue('drupal');
$drupal['live'] = $config->readValue('live');
$traefik = $this->config->readValue('traefik');
$traefik['usessl'] = 1;
$traefik['ports'] = 443;
$overwrite = [
'drupal' => $drupal,
'traefik' => $traefik,
'projectroot' => $root . '/app',
'projectrootdb' => $root . '/db',
'projectrootfiles' => $root . '/files',
'projectname' => getenv('CI_PROJECT_ID') . '-' . getenv('CI_COMMIT_BRANCH'),
'projectdomain' => $config->readValue('domain'),
'projectprotocol' => 'https',
'projectport' => '',
'extradomains' => $config->readValue('aliases'),
];
foreach ($overwrite as $key => $value) {
$this->config->setValue($key, $value, FALSE);
}
}
}
$fs = new Filesystem();
$installationManager = $this->composer->getInstallationManager();
$webRoot = $this->config->readValue('webroot');
if ($webRoot !== NULL) {
if (!$fs->exists($webRoot)) {
return;
}
}
else {
$drupalCorePackage = $this->getDrupalCorePackage();
if (!$drupalCorePackage) {
// We are called too early, Drupal core is not available yet.
return;
}
$corePath = $installationManager->getInstallPath($drupalCorePackage);
$webRoot = dirname($corePath);
$this->config->setValue('webRoot', $webRoot, FALSE);
$pluginRoot = $installationManager->getInstallPath($this->getPackage('lakedrops/docker4drupal'));
// If the d8-project-scaffold or d9-project-scaffold plugin is present we
// only execute this one if $force is TRUE. This way we can make sure that
// we get executed after d8-project-scaffold or d9-project-scaffold.

jurgenhaas
committed
$settingsPath = $webRoot . '/sites/default';
if ($this->getPackage('lakedrops/d8-project-scaffold') ||
$this->getPackage('lakedrops/d9-project-scaffold') ||
$this->getPackage('lakedrops/drupal-environment') ||
$this->getPackage('lakedrops/drupal-development-environment')) {

jurgenhaas
committed
if (!$fs->exists($projectRoot . '/settings/default')) {
return;
}
$settingsPath = 'settings/default';
}
foreach ($this->getFiles($projectRoot, $webRoot, $settingsPath) as $template => $def) {
if (isset($def['condition']) && !$def['condition']) {
continue;
}
if (!$fs->exists($def['dest'])) {
$fs->mkdir($def['dest']);
}
$filename = $this->config->render($template, $template);
$file = $def['dest'] . '/' . $filename;
if (($overwrite && empty($def['add2git'])) || !$fs->exists($file)) {
$rendered = $this->config->render($filename, file_get_contents($pluginRoot . '/templates/' . ($def['source'] ?? '') . $template . '.twig'));
$extraOptions = $this->config->readValue($filename);
if (!empty($def['add2yaml']) && $extraOptions !== NULL) {
$yaml = Yaml::parse($rendered);
/** @noinspection SlowArrayOperationsInLoopInspection */
$yaml = array_merge_recursive($yaml, $extraOptions);
$rendered = Yaml::dump($yaml, 9, 2);
// Render the string again so that custom content can also use variables
$rendered = $this->config->render($filename, $rendered);
elseif ($extraOptions !== NULL) {
$rendered .= $extraOptions;
}
continue;
}
$orig_file = $file . '.orig';
if ($fs->exists($orig_file)) {
$fs->remove($orig_file);
}
$fs->rename($file, $orig_file);
if (!$orig_ignored) {
if (empty($def['add2git'])) {
if (isset($def['link']) && ($def['link'] !== $settingsPath)) {
$link = $def['link'] . '/' . $filename;
if (!$fs->exists($link)) {
$rel = substr($fs->makePathRelative($file, $projectRoot . '/' . $link), 3, -1);
$fs->symlink($rel, $link);
}
}
$fs->chmod($file, 0664);
}
// Make sure that settings.docker.php gets called from settings.php.

jurgenhaas
committed
$settingsPhpFile = $settingsPath . '/settings.php';

jurgenhaas
committed
$settingsPhp = file_get_contents($settingsPhpFile);
if (strpos($settingsPhp, 'settings.docker.php') === FALSE) {

jurgenhaas
committed
$settingsPhp .= "\n\nif (file_exists(__DIR__ . '/settings.docker.php')) {\n include __DIR__ . '/settings.docker.php';\n}\n";

jurgenhaas
committed
file_put_contents($settingsPhpFile, $settingsPhp);
}
$this->gitIgnore('tests/backstop/backstop_data/bitmaps_test');
$this->gitIgnore('tests/backstop/backstop_data/html_report');
$this->gitLFS('tests/backstop/**/*.png');
if (getenv('LAKEDROPS_BUILD_NG') !== 'yes') {
$this->updateTraefik();
}
// Set permissions, see https://wodby.com/stacks/drupal/docs/local/permissions
exec('setfacl -dR -m u:$(whoami):rwX -m u:82:rwX -m u:100:rX -m g::rwX ' . $projectRoot . ' >/dev/null 2>&1');
exec('setfacl -R -m u:$(whoami):rwX -m u:82:rwX -m u:100:rX -m g::rwX ' . $projectRoot . ' >/dev/null 2>&1');
}
/**
* Configure Traefik on the host for all projects.
*/

jurgenhaas
committed
public function configureTraefik(): void {
if (!$this->isDevMode()) {
return;
}
$this->init();

jurgenhaas
committed
$this->updateTraefik();
}
/**

jurgenhaas
committed
* Update Traefik.

jurgenhaas
committed
private function updateTraefik(): void {
$traefik = new Traefik(
$this->config->readValue('projectname'),
$this->config->readValue(['traefik', 'domain']),
$this->config->readValue(['traefik', 'port']),
$this->config->readValue(['traefik', 'ports']),
$this->config->readValue(['traefik', 'cert']),
$this->config->readValue(['traefik', 'key'])
);

jurgenhaas
committed
if ($this->config->readValue(['traefik', 'portainer'])) {
$traefik->setAddonPortainer(TRUE);
}

jurgenhaas
committed
$traefik->update();
/**
* List of files and settings on how to handle them.
*
* @param string $projectRoot
* Name of the project's root directory.
* @param string $webRoot
* Name of the web's root directory.
* @param string $settingsPath
* Name of the settings directory.
*
* @return array
* List of files.
*/
protected function getFiles(string $projectRoot, string $webRoot, string $settingsPath): array {

jurgenhaas
committed
'dest' => $projectRoot . '/' . $settingsPath,
'add2yaml' => TRUE,

jurgenhaas
committed
'drushrc.php' => [
'dest' => $projectRoot . '/drush',
],
'default.site.yml' => [
'dest' => $projectRoot . '/drush/sites',
'add2yaml' => TRUE,
'stage.site.yml' => [
'dest' => $projectRoot . '/drush/sites',
'add2yaml' => TRUE,
],
'drush.yml' => [
'dest' => $projectRoot . '/drush',
'add2yaml' => TRUE,
'wkhtmltox.sh' => [
'dest' => $projectRoot . '/.docker-init',
],
'backstop.json' => [
'source' => 'tests/backstop/',
'dest' => $projectRoot . '/tests/backstop',
'add2yaml' => TRUE,
'add2git' => TRUE,
],
'vhost.conf' => [
'dest' => $projectRoot . '/apache',
'condition' => $this->config->readValue(['webserver', 'overwriteconfig']),

jurgenhaas
committed
'mhout.json' => [
'dest' => $projectRoot . '/tests',
],
/**
* Determine local ipv4 address.
*
* @param string|null $interface
* The name of the interface for which to determine the ipv4 address.
*
* @return string|array
* The ipv4 address(es).
*/

jurgenhaas
committed
private function getLocalIpv4(string $interface = NULL) {
$out = explode(PHP_EOL, shell_exec('LC_ALL=C /sbin/ifconfig'));
$local_addrs = array();
$ifname = 'unknown';
foreach ($out as $str) {
$matches = array();
if (preg_match('/^([a-z0-9]+)(:\d{1,2})?(\s)+Link/', $str, $matches)) {
$ifname = $matches[1];
}
elseif (preg_match('/inet addr:((?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:[.](?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3})\s/', $str, $matches)) {
$local_addrs[$ifname] = $matches[1];
}
}
if (!isset($interface)) {
return $local_addrs;
}
return $local_addrs[$interface] ?? '127.0.0.1';
/**
* @return string
*/
private function getDockerGateway(): string {
$container = $this->readContainerConfig();
return $container['NetworkSettings']['Gateway'];
}
* @param string $projectname
*
* @return string
*/
private function getDockerProxy(string $projectname): string {
if (isset($container['Name']) && in_array($container['Name'], ['traefik_traefik_1', 'traefik-traefik-1'])) {
return explode('/', $container['IPv4Address'])[0];
}
}
return '127.0.0.1';
}
/**
* @param $projectRoot
*
* @return string
*/
private function getDockerMountSource($projectRoot): string {
$currentDir = getcwd();
$container = $this->readContainerConfig();
foreach ($container['Mounts'] as $mount) {
if (empty($projectRoot)) {
if ($currentDir === $mount['Destination']) {
return $mount['Source'];
}
}
else if (strpos($projectRoot, $mount['Destination']) === 0) {
return $mount['Source'] . substr($projectRoot, strlen($mount['Destination']));
}
}
return getcwd();
}
/**
* @return array
*/
private function readContainerConfig(): array {
$output = [];
exec('basename "$(cat /proc/1/cpuset)"', $output);
$id = reset($output);
if ($id === '/') {
$id = getenv('COMPOSE_PROJECT_NAME') . '_l3d';
}
$output = [];
exec('docker container inspect ' . $id, $output);
return json_decode(implode('', $output), TRUE)[0];
// Ignore.
}
return [
'NetworkSettings' => [
'Gateway' => '127.0.0.1',
],
'Mounts' => [],
];
return json_decode(implode('', $output), TRUE)[0];
}
catch (Exception $ex) {
// Ignore.
}
return [
'Containers' => [],
];
}