<?php namespace LakeDrops\Docker4Drupal; use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksAccountLimitReachedException; use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksFailureException; use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksUnauthorisedException; use Henrywhitaker3\Healthchecks\Exceptions\HealthchecksUuidNotFoundException; use Henrywhitaker3\Healthchecks\Healthchecks; use Henrywhitaker3\Healthchecks\HealthchecksManager; use LakeDrops\Component\Composer\BaseHandler; use LakeDrops\DockerTraefik\Traefik; use LakeDrops\DrupalEnvironment\Handler as DrupalEnvironment; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Yaml\Yaml; /** * Handler for Docker4Drupal. * * @package LakeDrops\Docker4Drupal */ class Handler extends BaseHandler { /** * {@inheritdoc} */ public function configId(): string { return 'docker4drupal'; } /** * {@inheritdoc} */ protected function configDefault(): array { $dockerImageProxy = getenv('DOCKER_IMAGE_PREFIX'); if (!empty($dockerImageProxy)) { $dockerImageProxy = str_replace('//', '/', $dockerImageProxy . '/'); } $projectname = getenv('COMPOSE_PROJECT_NAME'); if (empty($projectname)) { $projectname = str_replace([' ', '-', '_', '.'], '', basename(getcwd())); $this->env->put('COMPOSE_PROJECT_NAME', $projectname); } $traefik_env = []; $i = 1; while ($item = $this->env->receive('traefik_env_' . $i, '', '0')) { [$key, $value] = explode(':', $item); $traefik_env[$key] = $value; $i++; } return [ 'docker_image_prefix' => $dockerImageProxy, 'projectname' => $projectname, 'staging' => FALSE, 'basicauth' => [ 'enable' => FALSE, 'user' => '', 'pass' => '', 'code' => '', ], 'ci_home' => '/home/gitlab-runner', 'docker0' => [ 'ip' => ($this->isCiContext() || $this->isLocalDevMode()) ? $this->getDockerGateway() : $this->getLocalIpv4('docker0'), 'proxy' => ($this->isCiContext() || $this->isLocalDevMode()) ? $this->getDockerProxy() : 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', ''), 'key' => $this->env->receive('traefik_key', ''), 'portainer' => $this->env->receive('traefik_portainer', '', '0'), 'hub_token' => $this->env->receive('traefik_hub_token', ''), 'dns_challenge' => $this->env->receive('traefik_dns_challenge', '', '0'), 'dns_challenge_provider' => $this->env->receive('traefik_dns_challenge_provider', ''), 'dns_challenge_resolver' => $this->env->receive('traefik_dns_challenge_resolver', ''), 'env' => $traefik_env, ], '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' => '9', ], 'php' => [ 'version' => $this->env->receiveGlobal('PHP_VERSION', 'PHP version', '8.2'), 'xdebug' => $this->env->receiveGlobal('PHP_DEBUG', 'PHP debug', '0'), 'coverage' => $this->env->receiveGlobal('PHP_COVERAGE', 'PHP coverage', '0'), 'profiler' => $this->env->receiveGlobal('PHP_PROFILER', 'PHP PROFILER', '0'), 'localip' => $this->env->receiveGlobal('LOCAL_IP', 'Local IP', '0'), 'related_subdomains' => [], ], 'dbserver' => [ 'type' => 'mariadb', 'version' => '11.2', ], 'webserver' => [ 'type' => 'apache', 'overwriteconfig' => FALSE, 'responseheader' => [ 'server' => '', 'strict_transport_security' => 'max-age=31536000; includeSubDomains', 'referrer_policy' => 'same-origin', 'permissions_policy' => 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), microphone=(), payment=(), usb=()', 'cross_origin_embedder_policy' => 'credentialless', 'cross_origin_opener_policy' => 'same-origin', 'cross_origin_resource_policy' => 'cross-origin', 'x_permitted_cross_domain_policies' => 'none', ], ], 'mailpit' => [ 'enable' => (getenv('LAKEDROPS_BUILD_NG') === 'yes' && in_array(getenv('CI_COMMIT_REF_SLUG'), ['master', 'main'], TRUE)) ? 0 : 1, 'routed' => (getenv('LAKEDROPS_BUILD_NG') === 'yes') ? 0 : 1, 'host' => $this->env->receiveGlobal('MAILPIT_HOST', 'MailPit Host', 'smtp.freesmtpservers.com'), 'port' => $this->env->receiveGlobal('MAILPIT_PORT', 'MailPit Port', '25'), 'username' => $this->env->receiveGlobal('MAILPIT_USERNAME', 'MailPit Username'), 'password' => $this->env->receiveGlobal('MAILPIT_PASSWORD', 'MailPit Password'), 'starttls' => $this->env->receiveGlobal('MAILPIT_STARTTLS', 'MailPit StartTLS'), 'allowinsecure' => $this->env->receiveGlobal('MAILPIT_ALLOW_INSECURE', 'MailPit allow insecure'), 'auth' => $this->env->receiveGlobal('MAILPIT_AUTH', 'MailPit Auth (none|plain|login|cram-md5)'), 'secret' => $this->env->receiveGlobal('MAILPIT_SECRET', 'MailPit Secret'), 'returnpath' => $this->env->receiveGlobal('MAILPIT_RETURNPATH', 'MailPit Bounce Address'), 'recipientallowlist' => $this->env->receiveGlobal('MAILPIT_RECIPIENT_ALLOW_LIST', 'MailPit Regex for allowed recipients'), ], 'symfony_mailer' => [ 'enable' => 0, 'host' => '', 'port' => 25, 'username' => '', 'password' => '', ], 'varnish' => [ 'enable' => 0, ], 'redis' => [ 'version' => '7', 'max_memory' => '1GB', ], '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, ], 'backstop' => $this->backstopDefaults(), 'crontabs' => [ 'www-data' => [], ], 'backup' => [ 'enable' => FALSE, 'version' => '1.8', 'crontime' => '50 */6 * * *', 'crontimecheck' => '30 23 1 * *', 'crontimecompact' => '30 23 5 * *', 'remoterepo' => FALSE, 'retention' => [ 'hourly' => 2, 'daily' => 7, 'weekly' => 8, 'monthly' => 12, 'yearly' => 30, ], ], 'cypress' => [ 'enable' => $this->env->receiveGlobal('CYPRESS', 'Cypress', '0'), 'version' => 'latest', ], 'unlighthouse' => [ 'enable' => $this->env->receiveGlobal('UNLIGHTHOUSE', 'Unlighthouse', '0'), 'urlprefix' => '', ], 'invoiceninja' => [ 'enable' => 0, ], 'tests' => [ 'behat' => FALSE, ], ]; } /** * {@inheritdoc} */ protected function postInit(): void { $this->config->setValue('docker', [ 'compose' => [ 'version' => $this->readDockerComposeVersion(), ], ], FALSE); $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('projectroot', $projectRoot, FALSE); $this->config->setValue('projectrootfiles', $projectRoot, FALSE); $this->config->setValue('projectrootinvoiceninja', $projectRoot . '/in', FALSE); $this->config->setValue('projectrootmailpit', $projectRoot . '/mailpit', FALSE); $this->config->setValue('projectdomain', str_replace('_', '-', $this->config->readValue('projectname')) . '.' . $traefik['domain'], FALSE); $this->config->setValue('projectprotocol', 'http' . ($traefik['usessl'] ? 's' : ''), FALSE); $this->config->setValue('socketprotocol', 'ws' . ($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); if (!empty($relatedprojectdomains)) { $php['localip'] = TRUE; } $this->config->setValue('php', $php, FALSE); // Get ID of Docker group. $docker_group_id = trim(shell_exec('stat -c "%g" /var/run/docker.sock')); $this->config->setValue('docker_group_id', $docker_group_id, FALSE); if ($alerta_api_key = getenv('ALERTA_APIKEY')) { $alerta = [ 'apikey' => $alerta_api_key, ]; if ($value = getenv('ALERTA_ENVIRONMENT')) { $alerta['environment'] = $value; } if ($value = getenv('ALERTA_PROJECT_ID')) { $alerta['project_id'] = $value; } $this->config->setValue('alerta', $alerta, FALSE); } // Turn off basic auth in local dev environment. if ($this->isLocalDevMode()) { $this->config->setValue('basicauth', ['enable' => FALSE], FALSE); } } /** * Update Drupal Project for Docker. */ public function updateProject(): void { // We only do the fancy stuff for developers. if (!$this->isDevMode()) { return; } $this->io->info('You may have to update your Docker4Drupal environment by running "composer lakedrops:docker4drupal".'); } /** * Configure Drupal Project for Docker. */ public function configureProject(): void { // We only do the fancy stuff for developers. if (!$this->isDevMode()) { return; } $this->init(); // Configure Drupal environment if available. 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(); $isStaging = !in_array(getenv('CI_COMMIT_REF_SLUG'), ['master', 'main'], TRUE); $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'); $crontabs['enable'] = (array_sum(array_map('count', $crontabs)) > 0); if (!$isStaging) { if ($this->config->readValue(['backup', 'enable'])) { $crontabs['borgmatic']['Backup'] = [ 'schedule' => $this->config->readValue(['backup', 'crontime']), 'command' => 'backup', ]; $crontabs['borgmatic']['Check backup'] = [ 'schedule' => $this->config->readValue(['backup', 'crontimecheck']), 'command' => 'check', ]; $crontabs['borgmatic']['Compact backup'] = [ 'schedule' => $this->config->readValue(['backup', 'crontimecompact']), 'command' => 'compact', ]; } $crontabs['www-data']['MySQL backup'] = [ 'schedule' => '5 0 * * *', 'command' => 'cd /var/www/html && /var/www/html/vendor/bin/drush sql:dump --result-file=/var/backups/mysql/drupal.sql', ]; $crontabs['enable'] = TRUE; $crontabs['enable_mysql_backup'] = TRUE; } $this->config->setValue('crontabs', $crontabs, FALSE); $overwriteConfig = [ 'staging' => $isStaging, 'docker0' => [ 'ip' => 'TRAEFIK-IP-PLACEHOLDER', 'proxy' => 'TRAEFIK-IP-PLACEHOLDER', ], 'drupal' => $drupal, 'traefik' => $traefik, 'projectroot' => $root . '/app', 'projectrootbackup' => $root . '/backup', 'projectrootdb' => $root . '/db', 'projectrootfiles' => $root . '/files', 'projectrootredis' => $root . '/redis', 'projectrootinvoiceninja' => $root . '/in', 'projectrootmailpit' => $root . '/mailpit', 'projectname' => 'drupal_' . getenv('PROJECT_NAME') . '_' . getenv('CI_COMMIT_REF_SLUG'), 'projectdomain' => str_replace('_', '-', $this->config->readValue('domain') ?? ''), 'projectprotocol' => 'https', 'projectport' => '', 'socketprotocol' => 'wss', 'extradomains' => $this->config->readValue('aliases') ?? [], ]; foreach ($overwriteConfig 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); // Directory where Drupal's index.php is located. $webRoot = basename(dirname($corePath)); } $this->config->setValue('webRoot', $webRoot, FALSE); // 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 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'; } // 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 (!empty($def['delete'])) { if ($fs->exists($file)) { $fs->remove($file); } continue; } if (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 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); } if (file_exists($file)) { $fs->chmod($file, 0664); } file_put_contents($file, $rendered); if (empty($def['add2git'])) { $fs->chmod($file, 0444); } } 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, $def['mode'] ?? 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 (!str_contains($settingsPhp, 'settings.docker.php')) { $settingsPhp .= "\n\nif (file_exists(__DIR__ . '/settings.docker.php')) {\n include __DIR__ . '/settings.docker.php';\n}\n"; $fs->chmod($settingsPhpFile, 0664); file_put_contents($settingsPhpFile, $settingsPhp); $fs->chmod($settingsPhpFile, 0444); } } // Setup BackstopJS. $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'); // Ignore some Cypress directories. $this->gitIgnore('tests/cypress/downloads'); $this->gitIgnore('tests/cypress/screenshots'); $this->gitIgnore('tests/cypress/videos'); // Ignore some Unlighthouse directories. $this->gitIgnore('tests/unlighthouse'); if ($this->config->readValue(['invoiceninja', 'enable'])) { // Ignore Invoice Ninja directory. $this->gitIgnore('/in/'); } if ($this->config->readValue(['mailpit', 'enable'])) { // Ignore mailpit directory. $this->gitIgnore('/mailpit/'); } 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(); $this->updateTraefik(); } /** * Update Traefik. */ private function updateTraefik(): void { if (getenv('TRAEFIK_HOST') === 'yes') { // Traefik is already available & controlled by the host. Don't touch it. return; } $traefik = new Traefik( $this->config->readValue('projectname'), $this->config->readValue(['traefik', 'domain']), (int) $this->config->readValue(['traefik', 'port']), (int) $this->config->readValue(['traefik', 'ports']), $this->config->readValue(['traefik', 'cert']), $this->config->readValue(['traefik', 'key']), $this->config->readValue(['traefik', 'env']), (bool) $this->config->readValue(['traefik', 'usessl']), (bool) $this->config->readValue(['traefik', 'dns_challenge']), $this->config->readValue(['traefik', 'dns_challenge_provider']), $this->config->readValue(['traefik', 'dns_challenge_resolver']) ); if ($this->config->readValue(['traefik', 'portainer'])) { $traefik->setAddonPortainer(TRUE); } if ($hub_token = $this->config->readValue(['traefik', 'hub_token'])) { $traefik->setHubToken($hub_token); } $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 { $files = [ 'settings.docker.php' => [ 'dest' => $projectRoot . '/' . $settingsPath, 'link' => $webRoot . '/sites/default', ], 'docker-compose.yml' => [ 'dest' => $projectRoot, 'add2yaml' => TRUE, ], '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']), ], 'mailpitout.yml' => [ 'dest' => $projectRoot . '/mailpit', ], 'in-vhost.conf' => [ 'dest' => $projectRoot . '/nginx', 'condition' => $this->config->readValue(['invoiceninja', 'enable']), ], ]; $files['cypress.config.js'] = [ 'source' => 'tests/', 'dest' => $projectRoot . '/tests', 'add2git' => TRUE, ]; $files['cypress-run'] = [ 'source' => 'tests/', 'dest' => $projectRoot . '/tests', 'add2git' => FALSE, 'mode' => 0775, ]; $files['commands.js'] = [ 'source' => 'tests/cypress/support/', 'dest' => $projectRoot . '/tests/cypress/support', 'add2git' => TRUE, ]; $files['e2e.js'] = [ 'source' => 'tests/cypress/support/', 'dest' => $projectRoot . '/tests/cypress/support', 'add2git' => TRUE, ]; $files['unlighthouse.config.ts'] = [ 'source' => 'tests/', 'dest' => $projectRoot . '/tests', 'add2git' => FALSE, ]; if (getenv('LAKEDROPS_BUILD_NG') === 'yes') { if ($this->config->readValue(['backup', 'enable'])) { $files['config.yaml'] = [ 'source' => 'backup/', 'dest' => $projectRoot . '/backup', ]; $files['crontab.txt'] = [ 'source' => 'backup/', 'dest' => $projectRoot . '/backup', ]; } // Manage crontabs and optionally add them to healthcheck-io. $hj_url = getenv('HEALTHCHECK_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_timezone = $this->env->receiveGlobal('HEALTHCHECK_API_TIMEZONE', '', 'Europe/Berlin'); $hj_checks = []; $hj_manager = NULL; if (!empty($hj_url) && !empty($hj_api_key)) { $hj_manager = new HealthchecksManager($hj_api_key, $hj_url . '/api/v1/'); try { $hj_checks = $hj_manager->listChecks(); } catch (HealthchecksFailureException | HealthchecksUnauthorisedException) { // Ignoring this for now. } } $activeTasks = []; foreach ($this->config->readValue('crontabs') ?? [] as $user => $tasks) { if (!is_array($tasks)) { continue; } foreach ($tasks as $name => $task) { $disabled = !empty($task['disabled']); $command = $task['command']; $schedule = $task['schedule']; $ping_url = FALSE; $task['name'] = $name; if (isset($hj_manager)) { unset($task['disabled'], $task['command']); $task['channels'] = $hj_api_channels; $task['tags'] = implode(' ', ['d4d', $hj_project, $hj_branch]); $task['tz'] = $hj_timezone; if (count(explode(' ', $task['schedule'])) === 6) { // Schedule contains seconds, they need to be removed. $task['schedule'] = substr($task['schedule'], strpos($task['schedule'], ' ') + 1); } $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); $parts = explode('/', $check['ping_url']); $uuid = array_pop($parts); $hj_check = new Healthchecks($uuid, $hj_url . '/ping/'); $hj_check->success(); } catch (HealthchecksFailureException | HealthchecksUuidNotFoundException | HealthchecksAccountLimitReachedException | HealthchecksUnauthorisedException) { // 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) { // Ignoring this for now. } } } if ($check) { $ping_url = $check['ping_url']; } } if (!$disabled) { if (count(explode(' ', $schedule)) === 5) { // Add leading "0" for seconds. $schedule = '0 ' . $schedule; } if ($user === 'borgmatic') { $backup = $this->config->readValue('backup'); $backup['healthckeck_url'][$command] = $ping_url; $this->config->setValue('backup', $backup, FALSE); } else { $activeTasks[] = [ 'name' => $task['name'], 'schedule' => $schedule, 'command' => $command, 'ping_url' => $ping_url, 'user' => $user, ]; } } } if (!empty($activeTasks)) { $files['jobs.ini'] = [ 'source' => 'crontabs/template.twig', 'dest' => $projectRoot . '/crontabs', 'options' => $activeTasks, ]; } } } return $files; } /** * 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): array|string { $out = explode(PHP_EOL, shell_exec('LC_ALL=C /sbin/ifconfig')); $local_addrs = []; $ifname = 'unknown'; foreach ($out as $str) { $matches = []; 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'; } /** * Get Docker gateway IP from docker inspect. * * @return string * The gateway IP. */ private function getDockerGateway(): string { $container = $this->readContainerConfig(); return $container['NetworkSettings']['Gateway']; } /** * Get Docker Proxy IP from docker inspect. * * @return string * The proxy IP. */ private function getDockerProxy(): string { foreach ($this->readNetworkConfig()['Containers'] as $container) { if (isset($container['Name']) && in_array($container['Name'], [ 'traefik', 'traefik_traefik_1', 'traefik-traefik-1', ])) { return explode('/', $container['IPv4Address'])[0]; } } return '127.0.0.1'; } /** * Get Docker mount source from docker inspect. * * @param string $projectRoot * The project root directory. * * @return string * The mount source. */ private function getDockerMountSource(string $projectRoot): string { $currentDir = getcwd(); $container = $this->readContainerConfig(); foreach ($container['Mounts'] as $mount) { if (empty($projectRoot)) { if ($currentDir === $mount['Destination']) { return $mount['Source']; } } elseif (str_starts_with($projectRoot, $mount['Destination'])) { return $mount['Source'] . substr($projectRoot, strlen($mount['Destination'])); } } return getcwd(); } /** * Get container details from docker inspect. * * @return array * The container details. */ private function readContainerConfig(): array { try { $testString = 'This is a test file for LakeDrops GitLab CI'; $filename = '/tmp/' . random_int(100, 999) . '.test'; file_put_contents($filename, $testString); $output = []; exec('docker ps -q', $output); $id = NULL; foreach ($output as $id) { $output = []; exec('docker exec ' . $id . ' cat ' . $filename . ' 2>&1', $output); if (reset($output) === $testString) { // Found the container. break; } } unlink($filename); if ($id !== NULL) { $output = []; exec('docker container inspect ' . $id, $output); return json_decode(implode('', $output), TRUE, 512, JSON_THROW_ON_ERROR)[0]; } } catch (\Exception) { // Ignore. } return [ 'NetworkSettings' => [ 'Gateway' => '127.0.0.1', ], 'Mounts' => [], ]; } /** * Get network configuration from docker inspect. * * @return array * The network configuration. */ private function readNetworkConfig(): array { try { $output = []; exec('docker network inspect traefik-public', $output); return json_decode(implode('', $output), TRUE, 512, JSON_THROW_ON_ERROR)[0]; } catch (\Exception) { // Ignore. } return [ 'Containers' => [], ]; } /** * Get the version from docker compose. * * @return string * The docker compose version. */ private function readDockerComposeVersion(): string { try { $output = []; exec('docker compose version --short', $output); return implode('', $output); } catch (\Exception) { // Ignore. } return '0.0.0'; } /** * Get default configuration for backstop. * * @return array * The default configuration for backstop. */ private function backstopDefaults(): array { return [ 'id' => 'drupal', 'd4dscripts' => [ 'before' => [], 'after' => [], ], 'viewports' => [ 'desktop' => [ 'width' => 1960, 'height' => 1280, ], ], 'scenarios' => [ 'frontpage' => [ 'path' => '/', ], ], ]; } }