Newer
Older
use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksAccountLimitReachedException;
use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksFailureException;
use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksUnauthorisedException;
use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksUuidNotFoundException;
use Henrywhitaker3\Healthchecks\HealthchecksManager;
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);
}
'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() :
'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,
],
'www-data' => [],
'version' => 'base-1.2.0-1.6.0',
'crontime' => '0 1 * * *',
'remoterepo' => FALSE,
'retention' => [
'hourly' => 2,
'daily' => 7,
'weekly' => 8,
'monthly' => 12,
'yearly' => 30,
],
],
}
/**
* {@inheritdoc}
*/
protected function postInit(): void {
$this->env->put('PHP_VERSION', $this->config->readValue(['php', 'version']), TRUE);
if ($this->isCiContext() || $this->isLocalDevMode()) {
$projectRoot = $this->getDockerMountSource(getenv('CI_PROJECT_DIR'));
}
else {
$projectRoot = getcwd();
}
$php = $this->config->readValue('php');
$traefik = $this->config->readValue('traefik');
// Check if SSH auth sockets are supported.
$ssh_auth_sock = getenv('SSH_AUTH_SOCK');
$php['ssh'] = !empty($ssh_auth_sock);
if ($php['ssh']) {
$php['ssh_auth_sock'] = ($this->isCiContext() || $this->isLocalDevMode()) ?
$this->getDockerMountSource('/ssh-agent') :
'$SSH_AUTH_SOCK';
}
$this->config->setValue('php', $php, FALSE);
$this->config->setValue('projectroot', $projectRoot, FALSE);
$this->config->setValue('projectdomain', $this->config->readValue('projectname') . '.' . $traefik['domain'], FALSE);
$this->config->setValue('projectprotocol', 'http' . ($traefik['usessl'] ? 's' : ''), FALSE);
$projectport = '';
if ($traefik['usessl'] && (int) $traefik['ports'] !== 443) {
$projectport = ':' . $traefik['ports'];
elseif (!$traefik['usessl'] && (int) $traefik['port'] !== 80) {
$projectport = ':' . $traefik['port'];
$this->config->setValue('projectport', $projectport, FALSE);
$relatedprojectdomains = [];
foreach ($php['related_subdomains'] as $related_subdomain) {
$relatedprojectdomains[] = $related_subdomain . '.' . $traefik['domain'];
}
$this->config->setValue('relatedprojectdomains', $relatedprojectdomains, FALSE);
* @param bool $overwrite

jurgenhaas
committed
public function configureProject(bool $overwrite = FALSE): void {
$this->init();
// 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;
$crontabs = $this->config->readValue('crontabs');
if ($this->config->readValue(['backup', 'enable'])) {
$crontabs['www-data']['MySQL backup'] = [
'schedule' => '5 0 * * *',
'command' => 'cd /var/www/html && /usr/local/bin/drush sql:dump --result-file=/var/backups/mysql/drupal.sql',
];
}
$crontabs['enable'] = (array_sum(array_map('count', $crontabs)) > 0);
$this->config->setValue('crontabs', $crontabs, FALSE);
$overwriteConfig = [
'staging' => !in_array(getenv('PROJECT_BRANCH_SANITIZED'), ['master', 'main'], TRUE),
'docker0' => [
'ip' => 'TRAEFIK-IP-PLACEHOLDER',
'proxy' => 'TRAEFIK-IP-PLACEHOLDER',
],
'drupal' => $drupal,
'traefik' => $traefik,
'projectroot' => $root . '/app',
'projectrootbackup' => $root . '/files/db',
'projectrootdb' => $root . '/db',
'projectrootfiles' => $root . '/files',
'projectname' => getenv('PROJECT_NAME') . '_' . getenv('CI_COMMIT_REF_SLUG'),
'projectdomain' => $this->config->readValue('domain') ?? '',
'projectprotocol' => 'https',
'projectport' => '',
'extradomains' => $this->config->readValue('aliases') ?? [],
$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 (!empty($def['delete'])) {
if ($fs->exists($file)) {
$fs->remove($file);
}
continue;
}
if (($overwrite && empty($def['add2git'])) || !$fs->exists($file)) {
$source = isset($def['source']) && is_file($pluginRoot . '/templates/' . $def['source']) ?
$pluginRoot . '/templates/' . $def['source'] :
$pluginRoot . '/templates/' . ($def['source'] ?? '') . $template . '.twig';
if (isset($def['options'])) {
$this->config->setValue('loopoptions', $def['options'], FALSE);
}
$rendered = $this->config->render($filename, file_get_contents($source));
$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);
}
}
// 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.json');
$this->gitIgnore('tests/backstop/backstop-script-*');
$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,
],
'backstop-script-before' => [
'source' => 'tests/backstop/',
'dest' => $projectRoot . '/tests/backstop',
'add2yaml' => TRUE,
'mode' => 0775,
],
'backstop-script-after' => [
'source' => 'tests/backstop/',
'dest' => $projectRoot . '/tests/backstop',
'add2yaml' => TRUE,
'mode' => 0775,
],
'vhost.conf' => [
'dest' => $projectRoot . '/apache',
'condition' => $this->config->readValue(['webserver', 'overwriteconfig']),

jurgenhaas
committed
'mhout.json' => [
'dest' => $projectRoot . '/tests',
],
$files['config.yaml'] = [
'source' => 'backup/',
'dest' => $projectRoot . '/backup',
'condition' => $this->config->readValue(['backup', 'enable']),
];
$files['crontab.txt'] = [
'source' => 'backup/',
'dest' => $projectRoot . '/backup',
'condition' => $this->config->readValue(['backup', 'enable']),
// Manage crontabs and optionally add them to heathcheck-io
$hj_api_url = getenv('HEALTHCHECK_API_URL');
$hj_api_key = getenv('HEALTHCHECK_API_KEY');
$hj_api_channels = getenv('HEALTHCHECK_API_CHANNELS');
$hj_project = $this->config->readValue('projectname');
$hj_branch = getenv('CI_COMMIT_BRANCH');
$hj_checks = [];
$hj_manager = NULL;
if (!empty($hj_api_url) && !empty($hj_api_key)) {
$hj_manager = new HealthchecksManager($hj_api_key, $hj_api_url);
try {
$hj_checks = $hj_manager->listChecks();
}
catch (HealthchecksFailureException | HealthchecksUnauthorisedException $e) {
// Ignoring this for now.
}
}
foreach ($this->config->readValue('crontabs') ?? [] as $user => $tasks) {
if (!is_array($tasks)) {
continue;
}
$activeTasks = [];
$disabled = !empty($task['disabled']);
$command = $task['command'];
if (isset($hj_manager)) {
unset($task['disabled'], $task['command']);
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
$task['channels'] = $hj_api_channels;
$task['tags'] = implode(' ', ['d4d', $hj_project, $hj_branch]);
$check = NULL;
foreach ($hj_checks as $existing) {
$tags = explode(' ', $existing['tags']);
if ($task['name'] === $existing['name'] && in_array('d4d', $tags, TRUE) && in_array($hj_project, $tags, TRUE) && in_array($hj_branch, $tags, TRUE)) {
$check = $existing;
break;
}
}
if (!$check) {
if ($disabled) {
// This task is disabled. As it doesn't exist yet, nothing to do.
continue;
}
try {
$check = $hj_manager->createCheck($task);
}
catch (HealthchecksAccountLimitReachedException | HealthchecksUnauthorisedException $e) {
// Ignoring this for now.
}
}
else {
$changed = FALSE;
foreach ($task as $key => $value) {
if (!isset($check[$key]) || $check[$key] !== $value) {
$changed = TRUE;
}
}
if ($changed) {
$parts = explode('/', $check['ping_url']);
$uuid = array_pop($parts);
try {
$check = $hj_manager->updateCheck($uuid, $task);
}
catch (HealthchecksFailureException | HealthchecksUuidNotFoundException | HealthchecksAccountLimitReachedException | HealthchecksUnauthorisedException $e) {
// Ignoring this for now.
}
}
}
if ($check) {
$command .= ' && curl -fsS --retry 5 -o /dev/null ' . $check['ping_url'];
}
}
if (!$disabled) {
}
}
if (empty($activeTasks)) {
$files[$user] = [
'dest' => $projectRoot . '/crontabs',
'delete' => TRUE,
];
}
else {
$files[$user] = [
'source' => 'crontabs/template.twig',
'dest' => $projectRoot . '/crontabs',
'options' => $activeTasks,
/**
* 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'];
}
/**
* @return string
*/
private function getDockerProxy(): 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];
catch (\Exception $ex) {
// Ignore.
}
return [
'NetworkSettings' => [
'Gateway' => '127.0.0.1',
],
'Mounts' => [],
];
return json_decode(implode('', $output), TRUE)[0];
}
catch (\Exception $ex) {
// Ignore.
}
return [
'Containers' => [],
];
}
/**
* @return array
*/
private function backstopDefaults(): array {
return [
'd4dscripts' => [
'before' => [],
'after' => [],
],
'viewports' => [
'desktop' => [
'width' => 1960,
'height' => 1280,
],
],
'scenarios' => [
'frontpage' => '/',
],
];
}