|
| 1 | +#!/usr/bin/env php |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * @file |
| 6 | + * Check project requirements or print system info. |
| 7 | + * |
| 8 | + * Usage: |
| 9 | + * doctor - Check project requirements. |
| 10 | + * doctor info - Show system information. |
| 11 | + * |
| 12 | + * IMPORTANT! This script runs outside the container on the host system. |
| 13 | + */ |
| 14 | + |
| 15 | +declare(strict_types=1); |
| 16 | + |
| 17 | +namespace DrevOps\VortexTooling; |
| 18 | + |
| 19 | +require_once __DIR__ . '/helpers.php'; |
| 20 | +execute_override(basename(__FILE__)); |
| 21 | + |
| 22 | +// ----------------------------------------------------------------------------- |
| 23 | + |
| 24 | +// Check minimal Doctor requirements. |
| 25 | +$check_minimal = getenv_default('VORTEX_DOCTOR_CHECK_MINIMAL', '0'); |
| 26 | + |
| 27 | +// Check pre-flight Doctor requirements. |
| 28 | +$check_preflight = getenv_default('VORTEX_DOCTOR_CHECK_PREFLIGHT', '0'); |
| 29 | + |
| 30 | +if ($check_minimal === '1') { |
| 31 | + putenv('VORTEX_DOCTOR_CHECK_PORT=0'); |
| 32 | + putenv('VORTEX_DOCTOR_CHECK_PYGMY=0'); |
| 33 | + putenv('VORTEX_DOCTOR_CHECK_SSH=0'); |
| 34 | + putenv('VORTEX_DOCTOR_CHECK_WEBSERVER=0'); |
| 35 | + putenv('VORTEX_DOCTOR_CHECK_BOOTSTRAP=0'); |
| 36 | +} |
| 37 | + |
| 38 | +if ($check_preflight === '1') { |
| 39 | + if (getenv('VORTEX_DOCTOR_CHECK_TOOLS') === FALSE) { |
| 40 | + putenv('VORTEX_DOCTOR_CHECK_TOOLS=1'); |
| 41 | + } |
| 42 | + if (getenv('VORTEX_DOCTOR_CHECK_PORT') === FALSE) { |
| 43 | + putenv('VORTEX_DOCTOR_CHECK_PORT=1'); |
| 44 | + } |
| 45 | + if (getenv('VORTEX_DOCTOR_CHECK_PYGMY') === FALSE) { |
| 46 | + putenv('VORTEX_DOCTOR_CHECK_PYGMY=1'); |
| 47 | + } |
| 48 | + if (getenv('VORTEX_DOCTOR_CHECK_CONTAINERS') === FALSE) { |
| 49 | + putenv('VORTEX_DOCTOR_CHECK_CONTAINERS=0'); |
| 50 | + } |
| 51 | + if (getenv('VORTEX_DOCTOR_CHECK_SSH') === FALSE) { |
| 52 | + putenv('VORTEX_DOCTOR_CHECK_SSH=0'); |
| 53 | + } |
| 54 | + if (getenv('VORTEX_DOCTOR_CHECK_WEBSERVER') === FALSE) { |
| 55 | + putenv('VORTEX_DOCTOR_CHECK_WEBSERVER=0'); |
| 56 | + } |
| 57 | + if (getenv('VORTEX_DOCTOR_CHECK_BOOTSTRAP') === FALSE) { |
| 58 | + putenv('VORTEX_DOCTOR_CHECK_BOOTSTRAP=0'); |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +// Check Doctor requirements for presence of tools. |
| 63 | +$check_tools = getenv_default('VORTEX_DOCTOR_CHECK_TOOLS', '1'); |
| 64 | + |
| 65 | +// Check Doctor requirements for port availability. |
| 66 | +$check_port = getenv_default('VORTEX_DOCTOR_CHECK_PORT', '1'); |
| 67 | + |
| 68 | +// Check Doctor requirements for Pygmy availability. |
| 69 | +$check_pygmy = getenv_default('VORTEX_DOCTOR_CHECK_PYGMY', '1'); |
| 70 | + |
| 71 | +// Check Doctor requirements for container status. |
| 72 | +$check_containers = getenv_default('VORTEX_DOCTOR_CHECK_CONTAINERS', '1'); |
| 73 | + |
| 74 | +// Check Doctor requirements for SSH key. |
| 75 | +$check_ssh = getenv_default('VORTEX_DOCTOR_CHECK_SSH', '1'); |
| 76 | + |
| 77 | +// Check Doctor requirements for webserver status. |
| 78 | +$check_webserver = getenv_default('VORTEX_DOCTOR_CHECK_WEBSERVER', '1'); |
| 79 | + |
| 80 | +// Check Doctor requirements for application bootstrap status. |
| 81 | +$check_bootstrap = getenv_default('VORTEX_DOCTOR_CHECK_BOOTSTRAP', '1'); |
| 82 | + |
| 83 | +// Default SSH key file. |
| 84 | +$ssh_file = getenv_default('VORTEX_SSH_FILE', (getenv('HOME') ?: '') . '/.ssh/id_rsa'); |
| 85 | + |
| 86 | +// ----------------------------------------------------------------------------- |
| 87 | + |
| 88 | +// Handle "info" subcommand. |
| 89 | +if (isset($GLOBALS['argv'][1]) && $GLOBALS['argv'][1] === 'info') { |
| 90 | + system_info(); |
| 91 | + quit(0); |
| 92 | +} |
| 93 | + |
| 94 | +command_must_exist('docker'); |
| 95 | +command_must_exist('pygmy'); |
| 96 | +command_must_exist('ahoy'); |
| 97 | + |
| 98 | +info('Checking project requirements'); |
| 99 | + |
| 100 | +if ($check_tools === '1') { |
| 101 | + if (!command_path('docker')) { |
| 102 | + fail('Please install Docker (https://www.docker.com/get-started).'); |
| 103 | + } |
| 104 | + if (!command_path('docker compose')) { |
| 105 | + fail('Please install docker compose (https://docs.docker.com/compose/install/).'); |
| 106 | + } |
| 107 | + if (!command_path('pygmy')) { |
| 108 | + fail('Please install Pygmy (https://pygmy.readthedocs.io/).'); |
| 109 | + } |
| 110 | + if (!command_path('ahoy')) { |
| 111 | + fail('Please install Ahoy (https://ahoy-cli.readthedocs.io/).'); |
| 112 | + } |
| 113 | + pass('All required tools are present.'); |
| 114 | +} |
| 115 | + |
| 116 | +if ($check_port === '1' && PHP_OS_FAMILY !== 'Linux') { |
| 117 | + passthru("lsof -i :80 2>/dev/null | grep -v 'CLOSED' | grep 'LISTEN' | grep -vq 'om.docke'", $exit_code); |
| 118 | + if ($exit_code === 0) { |
| 119 | + fail("Port 80 is occupied by a service other than Docker. Stop this service and run 'pygmy up'."); |
| 120 | + } |
| 121 | + pass('Port 80 is available.'); |
| 122 | +} |
| 123 | + |
| 124 | +if ($check_pygmy === '1') { |
| 125 | + passthru('pygmy status 2>/dev/null | tr -d "\\000" > /tmp/vortex_pygmy_status.txt', $exit_code); |
| 126 | + |
| 127 | + $pygmy_services = [ |
| 128 | + 'amazeeio-ssh-agent', |
| 129 | + 'amazeeio-mailhog', |
| 130 | + 'amazeeio-haproxy', |
| 131 | + 'amazeeio-dnsmasq', |
| 132 | + ]; |
| 133 | + |
| 134 | + foreach ($pygmy_services as $service) { |
| 135 | + passthru(sprintf('grep -q "%s: Running" /tmp/vortex_pygmy_status.txt', $service), $exit_code); |
| 136 | + if ($exit_code !== 0) { |
| 137 | + fail("Pygmy service %s is not running. Run 'pygmy up' or 'pygmy restart' to fix.", $service); |
| 138 | + } |
| 139 | + } |
| 140 | + pass('Pygmy is running.'); |
| 141 | +} |
| 142 | + |
| 143 | +if ($check_containers === '1') { |
| 144 | + $container_services = ['cli', 'php', 'nginx', 'database']; |
| 145 | + foreach ($container_services as $service) { |
| 146 | + passthru(sprintf('docker compose ps --status=running --services 2>/dev/null | grep -q %s', escapeshellarg($service)), $exit_code); |
| 147 | + if ($exit_code !== 0) { |
| 148 | + fail_no_exit('%s container is not running.', $service); |
| 149 | + note("Run 'ahoy up'."); |
| 150 | + note("Run 'ahoy logs %s' to see error logs.", $service); |
| 151 | + quit(1); |
| 152 | + } |
| 153 | + } |
| 154 | + pass('All containers are running'); |
| 155 | +} |
| 156 | + |
| 157 | +if ($check_ssh === '1') { |
| 158 | + $ssh_key_added_to_pygmy = FALSE; |
| 159 | + $ssh_key_volume_mounted = FALSE; |
| 160 | + |
| 161 | + // Check that the key is injected into pygmy ssh-agent container. |
| 162 | + passthru(sprintf('pygmy status 2>&1 | grep -q %s', escapeshellarg($ssh_file)), $exit_code); |
| 163 | + if ($exit_code !== 0) { |
| 164 | + fail_no_exit('SSH key is not added to pygmy.'); |
| 165 | + note("The SSH key will not be available in CLI container. Run 'pygmy restart' and then 'ahoy up'"); |
| 166 | + } |
| 167 | + else { |
| 168 | + $ssh_key_added_to_pygmy = TRUE; |
| 169 | + } |
| 170 | + |
| 171 | + // Check that the volume is mounted into CLI container. |
| 172 | + passthru('docker compose exec -T cli bash -c \'grep "^/dev" /etc/mtab | grep -q /tmp/amazeeio_ssh-agent\'', $exit_code); |
| 173 | + if ($exit_code !== 0) { |
| 174 | + fail_no_exit('SSH key volume is not mounted into CLI container.'); |
| 175 | + note('Make sure that your "docker-compose.yml" has the following lines for CLI service:'); |
| 176 | + note(' volumes_from:'); |
| 177 | + note(' - container:amazeeio-ssh-agent'); |
| 178 | + note("After adding these lines, run 'ahoy up'."); |
| 179 | + } |
| 180 | + else { |
| 181 | + $ssh_key_volume_mounted = TRUE; |
| 182 | + } |
| 183 | + |
| 184 | + // Check that ssh key is available in the container. |
| 185 | + if ($ssh_key_added_to_pygmy && $ssh_key_volume_mounted) { |
| 186 | + passthru("docker compose exec -T cli bash -c \"ssh-add -L | grep -q 'ssh-rsa'\"", $exit_code); |
| 187 | + if ($exit_code !== 0) { |
| 188 | + fail_no_exit("SSH key was not added into container. Run 'pygmy restart'."); |
| 189 | + } |
| 190 | + else { |
| 191 | + pass('SSH key is available within CLI container.'); |
| 192 | + } |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | +if ($check_webserver === '1') { |
| 197 | + $local_dev_url = trim((string) shell_exec("docker compose exec -T cli bash -c 'echo \${VORTEX_LOCALDEV_URL}' 2>/dev/null")); |
| 198 | + |
| 199 | + if ($local_dev_url !== '') { |
| 200 | + // Depending on the type of installation, the homepage may return 200 or 403. |
| 201 | + passthru(sprintf('curl -L -s -o /dev/null -w "%%{http_code}" %s 2>/dev/null | grep -q "200\\|403"', escapeshellarg($local_dev_url)), $exit_code); |
| 202 | + if ($exit_code !== 0) { |
| 203 | + fail('Web server is not accessible at http://%s.', $local_dev_url); |
| 204 | + } |
| 205 | + pass('Web server is running and accessible at http://%s.', $local_dev_url); |
| 206 | + |
| 207 | + if ($check_bootstrap === '1') { |
| 208 | + passthru(sprintf('curl -L -s -N %s 2>/dev/null | grep -q -i "charset="', escapeshellarg($local_dev_url)), $exit_code); |
| 209 | + if ($exit_code !== 0) { |
| 210 | + fail('Website is running, but cannot be bootstrapped. Check web-server configuration.'); |
| 211 | + } |
| 212 | + pass('Bootstrapped website at http://%s.', $local_dev_url); |
| 213 | + } |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +pass('All required checks have passed.'); |
| 218 | +echo PHP_EOL; |
| 219 | + |
| 220 | +// ----------------------------------------------------------------------------- |
| 221 | + |
| 222 | +/** |
| 223 | + * Sanitize system information output to remove PII data. |
| 224 | + */ |
| 225 | +function sanitize_system_info(string $input): string { |
| 226 | + $username = trim((string) shell_exec('whoami')); |
| 227 | + |
| 228 | + $input = str_replace('/Users/' . $username . '/', '/Users/[USERNAME_REDACTED]/', $input); |
| 229 | + $input = str_replace('/home/' . $username . '/', '/home/[USERNAME_REDACTED]/', $input); |
| 230 | + $input = str_replace($username, '[USERNAME_REDACTED]', $input); |
| 231 | + $input = (string) preg_replace('/ID: [a-f0-9-]{36}/', 'ID: [REDACTED]', $input); |
| 232 | + $input = (string) preg_replace('/[a-f0-9]{40,}/', '[HASH_REDACTED]', $input); |
| 233 | + $input = (string) preg_replace('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', '[IP_REDACTED]', $input); |
| 234 | + |
| 235 | + return $input; |
| 236 | +} |
| 237 | + |
| 238 | +/** |
| 239 | + * Print system information. |
| 240 | + */ |
| 241 | +function system_info(): void { |
| 242 | + echo 'System information report' . PHP_EOL; |
| 243 | + echo PHP_EOL; |
| 244 | + |
| 245 | + echo 'OPERATING SYSTEM' . PHP_EOL; |
| 246 | + if (PHP_OS === 'Darwin') { |
| 247 | + passthru('sw_vers'); |
| 248 | + } |
| 249 | + else { |
| 250 | + // @codeCoverageIgnoreStart |
| 251 | + passthru('lsb_release -a 2>/dev/null'); |
| 252 | + // @codeCoverageIgnoreEnd |
| 253 | + } |
| 254 | + echo PHP_EOL; |
| 255 | + |
| 256 | + echo 'DOCKER' . PHP_EOL; |
| 257 | + $docker_path = (string) command_path('docker'); |
| 258 | + echo 'Path to binary: ' . sanitize_system_info($docker_path) . PHP_EOL; |
| 259 | + passthru('docker -v'); |
| 260 | + $docker_info = (string) shell_exec('docker info 2>/dev/null'); |
| 261 | + if ($docker_info !== '') { |
| 262 | + echo sanitize_system_info($docker_info); |
| 263 | + } |
| 264 | + else { |
| 265 | + echo 'Docker is not running or not installed.' . PHP_EOL; |
| 266 | + } |
| 267 | + echo PHP_EOL; |
| 268 | + |
| 269 | + echo 'DOCKER COMPOSE V2' . PHP_EOL; |
| 270 | + echo 'Version: '; |
| 271 | + passthru('docker compose version 2>/dev/null || echo "Docker Compose V2 is not installed."'); |
| 272 | + echo PHP_EOL; |
| 273 | + |
| 274 | + echo 'DOCKER-COMPOSE V1' . PHP_EOL; |
| 275 | + $dc_path = (string) command_path('docker-compose'); |
| 276 | + echo 'Path to binary: ' . sanitize_system_info($dc_path) . PHP_EOL; |
| 277 | + echo 'Version: '; |
| 278 | + passthru('docker-compose version 2>/dev/null || echo "Docker Compose V1 is not installed."'); |
| 279 | + echo PHP_EOL; |
| 280 | + |
| 281 | + echo 'PYGMY' . PHP_EOL; |
| 282 | + $pygmy_path = (string) command_path('pygmy'); |
| 283 | + echo 'Path to binary: ' . sanitize_system_info($pygmy_path) . PHP_EOL; |
| 284 | + echo 'Version: '; |
| 285 | + passthru('pygmy version 2>/dev/null || echo "Pygmy is not installed."'); |
| 286 | + echo PHP_EOL; |
| 287 | + |
| 288 | + echo 'AHOY' . PHP_EOL; |
| 289 | + $ahoy_path = (string) command_path('ahoy'); |
| 290 | + echo 'Path to binary: ' . sanitize_system_info($ahoy_path) . PHP_EOL; |
| 291 | + echo 'Version: '; |
| 292 | + passthru('ahoy --version 2>/dev/null || echo "Ahoy is not installed."'); |
| 293 | + echo PHP_EOL; |
| 294 | +} |
0 commit comments