<?php namespace LakeDrops\Docker4Drupal; use LakeDrops\Component\Composer\BaseHandler; use LakeDrops\Component\Dotenv\Dotenv; use LakeDrops\DockerTraefik\Traefik; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Yaml\Yaml; /** * Class Handler. * * @package LakeDrops\Docker4Drupal */ class Handler extends BaseHandler { /** * @var array */ protected $options; /** * Configure Drupal Project for Docker. * * @param bool $overwrite * Whether to overwrite existing config files. * * @throws \Twig_Error_Loader * @throws \Twig_Error_Runtime * @throws \Twig_Error_Syntax */ public function configureProject($overwrite = FALSE) { // We only do the fancy stuff for developers. if (!$this->isDevMode()) { return; } $options = $this->getOptions(); $fs = new Filesystem(); $installationManager = $this->composer->getInstallationManager(); if (isset($options['webroot'])) { if (!$fs->exists($options['webroot'])) { return; } $webRoot = $options['webroot']; } else { $drupalCorePackage = $this->getDrupalCorePackage(); if (!$drupalCorePackage) { // We are called too early, Drupal core is not available yet. return; } $corePath = $installationManager->getInstallPath($drupalCorePackage); // Directory where Drupal's index.php is located. $webRoot = dirname($corePath); } // Directory where the root project is being created. $projectRoot = getcwd(); // Directory where this plugin is being installed. $pluginRoot = $installationManager->getInstallPath($this->getPackage('lakedrops/docker4drupal')); // If the d8-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. $settingsPath = $webRoot . '/sites/default'; if ($this->getPackage('lakedrops/d8-project-scaffold')) { if (!$fs->exists($projectRoot . '/settings/default')) { return; } $settingsPath = 'settings/default'; } // Provide all the required files. $twig_loader = new \Twig_Loader_Array([]); $twig = new \Twig_Environment($twig_loader); $options['webRoot'] = $webRoot . '/'; $orig_ignored = FALSE; foreach ($this->getFiles($projectRoot, $webRoot, $settingsPath) as $template => $def) { if (!$fs->exists($def['dest'])) { $fs->mkdir($def['dest']); } $twig_loader->setTemplate($template, $template); $filename = $twig->render($template, $options); $file = $def['dest'] . '/' . $filename; if ($overwrite || !$fs->exists($file)) { $twig_loader->setTemplate($filename, file_get_contents($pluginRoot . '/templates/' . ($def['source'] ?? '') . $template . '.twig')); $rendered = $twig->render($filename, $options); if (!empty($def['add2yaml']) && isset($options[$filename])) { $yaml = Yaml::parse($rendered); /** @noinspection SlowArrayOperationsInLoopInspection */ $yaml = array_merge_recursive($yaml, $options[$filename]); $rendered = Yaml::dump($yaml, 9, 2); } if ($fs->exists($file)) { if (md5_file($file) === md5($rendered)) { continue; } $orig_file = $file . '.orig'; if ($fs->exists($orig_file)) { $fs->remove($orig_file); } $fs->rename($file, $orig_file); if (!$orig_ignored) { $this->git('ignore *.orig'); $orig_ignored = TRUE; } } else if (empty($def['add2git'])) { $this->git('ignore ' . $filename); } file_put_contents($file, $rendered); } 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. $settingsPhpFile = $settingsPath . '/settings.php'; if ($fs->exists($settingsPhpFile)) { $settingsPhp = file_get_contents($settingsPhpFile); if (strpos($settingsPhp, 'settings.docker.php') === FALSE) { $settingsPhp .= "\n\nif (file_exists(__DIR__ . '/settings.docker.php')) {\n include __DIR__ . '/settings.docker.php';\n}\n"; file_put_contents($settingsPhpFile, $settingsPhp); } } // Setup BackstopJS. $this->git('ignore tests/backstop/backstop_data/bitmaps_test'); $this->git('ignore tests/backstop/backstop_data/html_report'); $this->git('lfs track tests/backstop/**/*.png'); $traefik = new Traefik($options['projectname']); $traefik->update(); // 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 ' . $projectRoot . ' >/dev/null 2>&1'); exec('setfacl -R -m u:$(whoami):rwX -m u:82:rwX -m u:100:rX ' . $projectRoot . ' >/dev/null 2>&1'); } /** * 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($projectRoot, $webRoot, $settingsPath): array { return [ 'settings.docker.php' => [ 'dest' => $projectRoot . '/' . $settingsPath, 'link' => $webRoot . '/sites/default', ], 'docker-compose.yml' => [ 'dest' => $projectRoot, 'add2yaml' => TRUE, ], 'aliases.drushrc.php' => [ 'dest' => $projectRoot . '/drush', ], 'drushrc.php' => [ 'dest' => $projectRoot . '/drush', ], 'default.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, ] ]; } /** * Retrieve options from composer.json "extra" configuration. * * @param null $key * Optional name of an option to be received instead of the full set of options. * * @return array|string * The options. */ public function getOptions($key = NULL) { if ($this->options === NULL) { $env = new Dotenv('docker4drupal', $this->io); $projectname = getenv('COMPOSE_PROJECT_NAME'); if (empty($projectname)) {$projectname = str_replace([' ', '-', '_', '.'], '', basename(getcwd())); $env->put('COMPOSE_PROJECT_NAME', $projectname); } $extra = $this->composer->getPackage()->getExtra() + ['docker4drupal' => []]; $options = NestedArray::mergeDeep([ 'projectname' => $projectname, 'ci_home' => '/home/gitlab-runner', 'docker0' => [ 'ip' => ($this->isCiContext() || $this->isLocalDevMode()) ? $this->getDockerGateway() : $this->getLocalIpv4('docker0'), ], 'live' => [ 'root' => '', 'uri' => '', 'host' => '', 'user' => $env->receive('live_host_username', 'Remote username for host of the live site', getenv('USER')), ], 'drush' => [ 'sql' => [ 'tables' => [ 'structure' => [ 'cache', 'cache_*', 'history', 'search_*', 'sessions', 'watchdog', ], 'skip' => [ 'migration_*', ], ], ], ], 'drupal' => [ 'version' => '8', ], 'php' => [ 'version' => '7.0', 'xdebug' => $this->isLocalDevMode() ? 1 : 0, ], 'webserver' => [ 'type' => 'apache', ], 'varnish' => [ 'enable' => 0, ], 'redis' => [ 'version' => '4.0', ], 'dbbrowser' => [ 'type' => 'pma', ], 'solr' => [ 'enable' => 0, 'version' => '6.6', ], 'node' => [ 'enable' => 0, 'key' => '', 'path' => '', ], 'memcached' => [ 'enable' => 0, ], 'rsyslog' => [ 'enable' => 0, ], 'athenapdf' => [ 'enable' => 0, 'key' => '', ], 'blackfire' => [ 'enable' => 0, 'id' => '', 'token' => '', ], 'webgrind' => [ 'enable' => 0, ], 'wkhtmltox' => [ 'enable' => 0, ], ], $extra['docker4drupal']); 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; $this->options = $env->replaceEnvironmentVariables($options); $env->put('PHP_VERSION', $this->options['php']['version'], TRUE); } if ($key !== NULL) { return $this->options[$key]; } return $this->options; } /** * 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). */ private function getLocalIpv4($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]; if ($matches[2] !== '') { $ifname .= $matches[2]; } } 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 $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(); } private function readContainerConfig() { try { $output = []; exec('basename "$(cat /proc/1/cpuset)"', $output); $id = reset($output); $output = []; exec('docker container inspect ' . $id, $output); return json_decode(implode('', $output), TRUE)[0]; } catch (\Exception $ex) { // Ignore. } return []; } }