Newer
Older
use LakeDrops\Component\Composer\BaseHandler;
use LakeDrops\DockerTraefik\Traefik;

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'),
],

jurgenhaas
committed
'dbserver' => [
'type' => 'mariadb',
'version' => '10.5',
],
'webserver' => [
'type' => 'apache',
'overwriteconfig' => FALSE,
],

jurgenhaas
committed
'mailhog' => [
'host' => $this->env->receiveGlobal('MAILHOG_HOST', 'smtp.freesmtpservers.com'),
'port' => $this->env->receiveGlobal('MAILHOG_PORT', '25'),
'username' => $this->env->receiveGlobal('MAILHOG_USERNAME', ''),
'password' => $this->env->receiveGlobal('MAILHOG_PASSWORD', ''),
'mechanism' => $this->env->receiveGlobal('MAILHOG_MECHANISM', 'NONE'),
],
'varnish' => [
'enable' => 0,
],
'redis' => [
],
'dbbrowser' => [
'type' => 'pma',
],
'solr' => [
'enable' => 0,
'version' => '4.8.0',
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
],
'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,
],
];
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;
return $options;
}
/**
* {@inheritdoc}
*/
protected function postInit(): void {
$this->env->put('PHP_VERSION', $this->config->readValue(['php', 'version']), TRUE);
* @param bool $overwrite
public function configureProject($overwrite = FALSE): void {
$this->init();
$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');
$this->updateTraefik(FALSE);
// 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.
*
* @param bool $rewrite
* Whether to rewrite existing Traefik config.
*/
public function configureTraefik($rewrite = FALSE): void {
if (!$this->isDevMode()) {
return;
}
$this->init();
$this->updateTraefik($rewrite);
}
/**
* @param $rewrite
*/
private function updateTraefik($rewrite): 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 ((bool) $this->config->readValue(['traefik', 'portainer'])) {
$traefik->setAddonPortainer(TRUE);
}
$traefik->update($rewrite);
/**
* 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).
*/
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];
}
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 {
foreach ($this->readNetworkConfig($projectname)['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'];
}
}
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' => [],
];
/**
* @param string $projectname
*
* @return array
*/
private function readNetworkConfig(string $projectname): array {
try {
$output = [];
exec('docker network inspect traefik_' . $projectname, $output);
return json_decode(implode('', $output), TRUE)[0];
}
catch (Exception $ex) {
// Ignore.
}
return [
'Containers' => [],
];
}