Skip to content
Snippets Groups Projects
Handler.php 17.5 KiB
Newer Older
jurgenhaas's avatar
jurgenhaas committed
<?php

namespace LakeDrops\Docker4Drupal;

jurgenhaas's avatar
jurgenhaas committed
use Exception;
use LakeDrops\Component\Composer\BaseHandler;
use LakeDrops\DockerTraefik\Traefik;
use LakeDrops\DrupalEnvironment\Handler as DrupalEnvironment;
jurgenhaas's avatar
jurgenhaas committed
use Symfony\Component\Filesystem\Filesystem;
jurgenhaas's avatar
jurgenhaas committed

jurgenhaas's avatar
jurgenhaas committed
/**
 * Class Handler.
 *
 * @package LakeDrops\Docker4Drupal
 */
class Handler extends BaseHandler {
jurgenhaas's avatar
jurgenhaas committed

  public function configId(): string {
    return 'docker4drupal';
  }

  /**
   * {@inheritdoc}
   */
  protected function configDefault(): array {
    $projectname = getenv('COMPOSE_PROJECT_NAME');
jurgenhaas's avatar
jurgenhaas committed
    if (empty($projectname)) {
      $projectname = str_replace([' ', '-', '_', '.'], '', basename(getcwd()));
      $this->env->put('COMPOSE_PROJECT_NAME', $projectname);
    }

    $options = [
      'projectname' => $projectname,
jurgenhaas's avatar
jurgenhaas committed
      'staging' => FALSE,
jurgenhaas's avatar
jurgenhaas committed
      'basicauth' => [
        'enabled' => FALSE,
jurgenhaas's avatar
jurgenhaas committed
        'user' => '',
        'pass' => '',
        'code' => '',
jurgenhaas's avatar
jurgenhaas committed
      ],
      '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'),
        'version' => '10.6',
      'webserver' => [
        'type' => 'apache',
        'overwriteconfig' => FALSE,
      ],
        'host' => $this->env->receiveGlobal('MAILHOG_HOST', 'MailHog Host', 'smtp.freesmtpservers.com'),
        'port' => $this->env->receiveGlobal('MAILHOG_PORT', 'MailHog Port', '25'),
        'username' => $this->env->receiveGlobal('MAILHOG_USERNAME', 'MailHog Username'),
        'password' => $this->env->receiveGlobal('MAILHOG_PASSWORD', 'MailHog Password'),
        'mechanism' => $this->env->receiveGlobal('MAILHOG_MECHANISM', 'MailHog Auth Mechanism', 'NONE'),
      'varnish' => [
        'enable' => 0,
      ],
      'redis' => [
      ],
      'dbbrowser' => [
        'type' => 'pma',
      ],
      'solr' => [
        'enable' => 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);
jurgenhaas's avatar
jurgenhaas committed
  /**
   * Configure Drupal Project for Docker.
   *
jurgenhaas's avatar
jurgenhaas committed
   *   Whether to overwrite existing config files.
jurgenhaas's avatar
jurgenhaas committed
   */
  public function configureProject(bool $overwrite = FALSE): void {
jurgenhaas's avatar
jurgenhaas committed

jurgenhaas's avatar
jurgenhaas committed
    // We only do the fancy stuff for developers.
    if (!$this->isDevMode()) {
jurgenhaas's avatar
jurgenhaas committed
      return;
    }

    // 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;
jurgenhaas's avatar
jurgenhaas committed
        $overwriteConfig = [
          'staging' => !in_array(getenv('PROJECT_BRANCH_SANITIZED'), ['master', 'main'], TRUE),
jurgenhaas's avatar
jurgenhaas committed
          'docker0' => [
            'ip' => 'TRAEFIK-IP-PLACEHOLDER',
            'proxy' => 'TRAEFIK-IP-PLACEHOLDER',
          ],
          'drupal' => $drupal,
          'traefik' => $traefik,
          'projectroot' => $root . '/app',
          'projectrootdb' => $root . '/db',
          'projectrootfiles' => $root . '/files',
jurgenhaas's avatar
jurgenhaas committed
          'projectrootredis' => $root . '/redis',
jurgenhaas's avatar
jurgenhaas committed
          'projectname' => getenv('PROJECT_NAME') . '_' . getenv('CI_COMMIT_REF_SLUG'),
          'projectdomain' => $config->readValue('domain'),
          'projectprotocol' => 'https',
          'projectport' => '',
          'extradomains' => $config->readValue('aliases'),
        ];
jurgenhaas's avatar
jurgenhaas committed
        foreach ($overwriteConfig as $key => $value) {
          $this->config->setValue($key, $value, FALSE);
        }
      }
    }

jurgenhaas's avatar
jurgenhaas committed
    $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);
jurgenhaas's avatar
jurgenhaas committed
      // Directory where Drupal's index.php is located.
      $webRoot = dirname($corePath);
    $this->config->setValue('webRoot', $webRoot, FALSE);
jurgenhaas's avatar
jurgenhaas committed

jurgenhaas's avatar
jurgenhaas committed
    // Directory where the root project is being created.
jurgenhaas's avatar
jurgenhaas committed
    $projectRoot = getcwd();
jurgenhaas's avatar
jurgenhaas committed
    // Directory where this plugin is being installed.
jurgenhaas's avatar
jurgenhaas committed
    $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.
    $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')) {
      if (!$fs->exists($projectRoot . '/settings/default')) {
        return;
      }
      $settingsPath = 'settings/default';
    }

jurgenhaas's avatar
jurgenhaas committed
    // Provide all the required files.
    $orig_ignored = FALSE;
    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) {
          /** @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;
        }
        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->gitIgnore('*.orig');
            $orig_ignored = TRUE;
          }
        }
        if (empty($def['add2git'])) {
          $this->gitIgnore($filename);
jurgenhaas's avatar
jurgenhaas committed
        file_put_contents($file, $rendered);
      }
      if (isset($def['link']) && ($def['link'] !== $settingsPath)) {
        $link = $def['link'] . '/' . $filename;
jurgenhaas's avatar
jurgenhaas committed
        if (!$fs->exists($link)) {
          $rel = substr($fs->makePathRelative($file, $projectRoot . '/' . $link), 3, -1);
          $fs->symlink($rel, $link);
        }
      }
      $fs->chmod($file, 0664);
    }

jurgenhaas's avatar
jurgenhaas committed
    // 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->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.
   */
  public function configureTraefik(): void {
    if (!$this->isDevMode()) {
      return;
    }
    $this->init();
  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'])
    );
    if ($this->config->readValue(['traefik', 'portainer'])) {
      $traefik->setAddonPortainer(TRUE);
    }
jurgenhaas's avatar
jurgenhaas committed
  }

jurgenhaas's avatar
jurgenhaas committed
  /**
   * 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's avatar
jurgenhaas committed
    return [
      'settings.docker.php' => [
        'dest' => $projectRoot . '/' . $settingsPath,
jurgenhaas's avatar
jurgenhaas committed
        'link' => $webRoot . '/sites/default',
      ],
jurgenhaas's avatar
jurgenhaas committed
      'docker-compose.yml' => [
jurgenhaas's avatar
jurgenhaas committed
        'dest' => $projectRoot,
jurgenhaas's avatar
jurgenhaas committed
      ],
      'drushrc.php' => [
        'dest' => $projectRoot . '/drush',
      ],
      'default.site.yml' => [
        'dest' => $projectRoot . '/drush/sites',
jurgenhaas's avatar
jurgenhaas committed
      'stage.site.yml' => [
        'dest' => $projectRoot . '/drush/sites',
        'add2yaml' => TRUE,
      ],
      'drush.yml' => [
        'dest' => $projectRoot . '/drush',
      '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']),
      'mhout.json' => [
        'dest' => $projectRoot . '/tests',
      ],
jurgenhaas's avatar
jurgenhaas committed
    ];
  }

jurgenhaas's avatar
jurgenhaas committed
  /**
   * 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(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];
        if ($matches[2] !== '') {
          $ifname .= $matches[2];
        }
jurgenhaas's avatar
jurgenhaas committed
      }
      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 $projectname): string {
jurgenhaas's avatar
jurgenhaas committed
    foreach ($this->readNetworkConfig()['Containers'] as $container) {
      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'];
        }
      }
jurgenhaas's avatar
jurgenhaas committed
      else if (strpos($projectRoot, $mount['Destination']) === 0) {
        return $mount['Source'] . substr($projectRoot, strlen($mount['Destination']));
  /**
   * @return array
   */
  private function readContainerConfig(): array {
jurgenhaas's avatar
jurgenhaas committed
      $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];
jurgenhaas's avatar
jurgenhaas committed
    catch (Exception $ex) {
    return [
      'NetworkSettings' => [
        'Gateway' => '127.0.0.1',
      ],
      'Mounts' => [],
    ];
jurgenhaas's avatar
jurgenhaas committed
  private function readNetworkConfig(): array {
    try {
      $output = [];
jurgenhaas's avatar
jurgenhaas committed
      exec('docker network inspect traefik-public', $output);
      return json_decode(implode('', $output), TRUE)[0];
    }
    catch (Exception $ex) {
      // Ignore.
    }
    return [
      'Containers' => [],
    ];
  }

jurgenhaas's avatar
jurgenhaas committed
}