diff --git a/.cursor/commands/review-pr-comment.md b/.cursor/commands/review-pr-comment.md new file mode 100644 index 00000000..02630feb --- /dev/null +++ b/.cursor/commands/review-pr-comment.md @@ -0,0 +1 @@ +Please assess whether the concerns raised in the following PR comment are valid, and propose possible solutions to address them. diff --git a/.cursor/rules/06-playbooks.mdc b/.cursor/rules/06-playbooks.mdc index e2fe1125..d22d225d 100644 --- a/.cursor/rules/06-playbooks.mdc +++ b/.cursor/rules/06-playbooks.mdc @@ -39,7 +39,7 @@ export DEBIAN_FRONTEND=noninteractive # ---- main() { - echo "✓ Starting..." + echo "→ Starting..." # Tasks go here @@ -69,7 +69,7 @@ main "$@" Use `DEPLOYER_` prefix. Standard variables: - `DEPLOYER_OUTPUT_FILE` - YAML output path (provided automatically) -- `DEPLOYER_DISTRO` - Distribution: `debian|redhat|amazon` (if needed) +- `DEPLOYER_DISTRO` - Distribution: `ubuntu|debian` (if needed) - `DEPLOYER_PERMS` - Permissions: `root|sudo|none` (if needed) **Validation:** @@ -78,24 +78,26 @@ Detection playbooks only validate `DEPLOYER_OUTPUT_FILE`. Provisioning playbooks ### Distribution Support -Support Debian, RedHat, Amazon Linux. Use `case` statements for package operations: +Support Ubuntu and Debian only (both use apt package manager). Use `case` statements when Ubuntu/Debian need different package names or configurations: ```bash -# ✅ CORRECT - case statement for package managers +# ✅ CORRECT - case statement when distributions differ case $DEPLOYER_DISTRO in - debian) - run_cmd apt-get update -q - run_cmd apt-get install -y -q "$package" + ubuntu) + distro_packages=(software-properties-common) + run_cmd apt-get install -y "${distro_packages[@]}" ;; - redhat|amazon) - run_cmd yum install -y -q "$package" + debian) + distro_packages=(apt-transport-https lsb-release ca-certificates) + run_cmd apt-get install -y "${distro_packages[@]}" ;; esac -# ❌ WRONG - Unnecessary branching for universal operations +# ❌ WRONG - Unnecessary branching for identical operations case $DEPLOYER_DISTRO in - debian|redhat|amazon) - run_cmd systemctl start service # Same everywhere! + ubuntu|debian) + run_cmd apt-get update -q # Same for both! + run_cmd apt-get install -y -q caddy # Same for both! ;; esac ``` @@ -103,6 +105,8 @@ esac **Universal operations (no branching needed):** ```bash +run_cmd apt-get update -q +run_cmd apt-get install -y -q caddy run_cmd systemctl start caddy run_cmd systemctl enable caddy run_cmd mkdir -p /var/www/app @@ -136,9 +140,22 @@ if ! systemctl is-enabled --quiet caddy; then run_cmd systemctl enable --quiet caddy fi +# For config files that may exist (from packages), check for custom content markers +if ! grep -q "import conf.d/localhost.caddy" /etc/caddy/Caddyfile 2> /dev/null; then + echo "→ Creating Caddyfile with custom configuration..." + run_cmd tee /etc/caddy/Caddyfile > /dev/null <<- 'EOF' + # ... custom config with marker ... + EOF +fi + # ❌ WRONG - Not idempotent run_cmd useradd deployer # Fails second time echo "export PATH=\$PATH:/usr/local/bin" >> ~/.bashrc # Duplicates each run + +# ❌ WRONG - File existence check when package installs default config +if ! run_cmd test -f /etc/caddy/Caddyfile; then + # This will never run if package created a default file! +fi ``` ### Error Handling @@ -157,7 +174,7 @@ fi # Silent checks (expected to sometimes fail) if ! command -v nginx >/dev/null 2>&1; then - echo "✓ Installing nginx..." + echo "→ Installing nginx..." run_cmd apt-get install -y -q nginx fi @@ -190,6 +207,22 @@ run_cmd() { The `-n` flag ensures sudo fails fast without prompting for a password, maintaining non-interactive operation. +**Sourcing Shared Helpers:** + +Playbooks use shared helper functions from `helpers.sh`. These helpers are automatically inlined when executing playbooks remotely, so playbooks should include a commented source line: + +```bash +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" +``` + +**Rules:** + +- NEVER manually inline helpers into playbook files +- Keep the commented source line for documentation +- Helpers are inlined automatically during remote execution +- The comment pattern allows local testing if needed while documenting the dependency + ### Output Write YAML to `$DEPLOYER_OUTPUT_FILE`. Progress messages to stdout/stderr. @@ -197,9 +230,9 @@ Write YAML to `$DEPLOYER_OUTPUT_FILE`. Progress messages to stdout/stderr. **Pattern:** ```bash -# Progress messages (stdout) -echo "✓ Processing..." -echo "✓ Task complete" +# Action messages (stdout) - indicate what's about to happen +echo "→ Installing packages..." +echo "→ Configuring service..." # YAML output to file (check for errors) if ! cat > "$DEPLOYER_OUTPUT_FILE" <&2 + exit 1 +fi + +# ✅ CORRECT - Conditional operations (message INSIDE block, only when needed) +if ! [[ -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg ]]; then + echo "→ Adding Caddy GPG key..." + if ! curl -1sLf 'https://example.com/key.gpg' | run_cmd gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg; then + echo "Error: Failed to add Caddy GPG key" >&2 + exit 1 + fi +fi + +# ❌ WRONG - Message outside conditional (shows even when nothing happens) +echo "→ Configuring Caddy repository..." +if ! [[ -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg ]]; then + # GPG key logic... +fi +``` + +**Rules:** Be explicit with paths/names/versions. Place messages INSIDE conditional blocks for idempotent operations, OUTSIDE only for operations that always run. Never write progress to output file. See: `playbooks/package-manager.sh` ### Complete Example @@ -256,26 +323,19 @@ run_cmd() { main() { local caddy_version - echo "✓ Installing Caddy..." if ! command -v caddy >/dev/null 2>&1; then - case $DEPLOYER_DISTRO in - debian) - run_cmd apt-get update -q - run_cmd apt-get install -y -q caddy - ;; - redhat|amazon) - run_cmd yum install -y -q caddy - ;; - esac + echo "→ Installing Caddy web server..." + run_cmd apt-get update -q + run_cmd apt-get install -y -q caddy fi - echo "✓ Creating directory..." if [[ ! -d /var/www/app ]]; then + echo "→ Creating /var/www/app directory..." run_cmd mkdir -p /var/www/app fi - echo "✓ Enabling service..." if ! systemctl is-enabled --quiet caddy; then + echo "→ Enabling Caddy service..." run_cmd systemctl enable --quiet caddy fi diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index ee0cc13f..84d5f0dd 100644 --- a/app/Console/Server/ServerInstallCommand.php +++ b/app/Console/Server/ServerInstallCommand.php @@ -5,10 +5,10 @@ namespace Bigpixelrocket\DeployerPHP\Console\Server; use Bigpixelrocket\DeployerPHP\Contracts\BaseCommand; +use Bigpixelrocket\DeployerPHP\DTOs\ServerDTO; use Bigpixelrocket\DeployerPHP\Enums\Distribution; use Bigpixelrocket\DeployerPHP\Traits\PlaybooksTrait; use Bigpixelrocket\DeployerPHP\Traits\ServersTrait; -use GuzzleHttp\Client; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -34,7 +34,8 @@ protected function configure(): void $this->addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name'); $this->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version to install'); - $this->addOption('php-default', null, InputOption::VALUE_NONE, 'Set as default PHP version'); + $this->addOption('php-default', null, InputOption::VALUE_NEGATABLE, 'Set as default PHP version'); + $this->addOption('php-extensions', null, InputOption::VALUE_REQUIRED, 'Comma-separated PHP extensions'); } // ---- @@ -78,42 +79,101 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var string $permissions */ // - // Execute installation playbook + // Prepare packages + // ---- + + $packageList = $this->executePlaybook( + $server, + 'package-list', + 'Preparing packages...', + [ + 'DEPLOYER_DISTRO' => $distro, + 'DEPLOYER_PERMS' => $permissions, + 'DEPLOYER_GATHER_PHP' => 'true', + ], + true + ); + + if (is_int($packageList)) { + return $packageList; + } + + // + // Install base packages // ---- $result = $this->executePlaybook( $server, - 'server-install', - 'Installing server...', + 'install-base', + 'Installing base packages...', [ 'DEPLOYER_DISTRO' => $distro, 'DEPLOYER_PERMS' => $permissions, - 'DEPLOYER_SERVER_NAME' => $server->name, ], true ); if (is_int($result)) { - $this->io->error('Server installation failed'); - return $result; } - $this->yay('Server installed successfully'); - // // Install PHP // ---- - $phpResult = $this->installPhp($server, $info); + $phpResult = $this->installPhp($server, $info, $packageList); if (is_int($phpResult)) { return $phpResult; } - /** @var array{status: int, php_version: string, php_default: bool} $phpResult */ + /** @var array{status: int, php_version: string, php_default: bool, php_default_prompted: bool, php_extensions: string} $phpResult */ $phpVersion = $phpResult['php_version']; $phpDefault = $phpResult['php_default']; + $phpDefaultPrompted = $phpResult['php_default_prompted']; + $phpExtensions = $phpResult['php_extensions']; + + // + // Install Bun + // ---- + + $bunResult = $this->executePlaybook( + $server, + 'install-bun', + 'Installing Bun...', + [ + 'DEPLOYER_PERMS' => $permissions, + ], + true + ); + + if (is_int($bunResult)) { + return $bunResult; + } + + // + // Setup deployer user + // ---- + + $deployerResult = $this->executePlaybook( + $server, + 'install-deployer', + 'Setting up deployer user...', + [ + 'DEPLOYER_DISTRO' => $distro, + 'DEPLOYER_PERMS' => $permissions, + 'DEPLOYER_SERVER_NAME' => $server->name, + ], + true + ); + + if (is_int($deployerResult)) { + $this->io->error('Deployer user setup failed'); + + return $deployerResult; + } + + $this->yay('Deployer user setup successful'); // // Setup demo site @@ -125,6 +185,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'demo-site', 'Setting up demo site...', [ + 'DEPLOYER_DISTRO' => $distro, 'DEPLOYER_PERMS' => $permissions, ], true @@ -142,10 +203,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Verify installation // ---- - $url = 'http://' . $server->host; - $deployKey = isset($result['deploy_public_key']) && is_string($result['deploy_public_key']) && $result['deploy_public_key'] !== 'unknown' - ? $result['deploy_public_key'] - : null; + // IPv6 addresses must be wrapped in brackets for URLs + $host = $server->host; + if (str_contains($host, ':')) { + // Any IPv6 literal must be wrapped in brackets + $url = "http://[{$host}]"; + } else { + $url = "http://{$host}"; + } + /** @var string|null $deployKey */ + $deployKey = $deployerResult['deploy_public_key'] ?? null; $verification = $this->io->promptSpin( fn () => $this->verifyInstallation($url, $deployKey), @@ -166,15 +233,242 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Show command replay // ---- - $this->showCommandReplay('server:install', [ + $replayOptions = [ 'server' => $server->name, 'php-version' => $phpVersion, - 'php-default' => $phpDefault, - ]); + 'php-extensions' => $phpExtensions, + ]; + + if ($phpDefaultPrompted) { + $replayOptions['php-default'] = $phpDefault; + } + + $this->showCommandReplay('server:install', $replayOptions); return Command::SUCCESS; } + // + // PHP Installation + // ---- + + /** + * Install PHP on a server. + * + * Prompts for PHP version selection and handles installation via playbook. + * Automatically sets first PHP install as default, otherwise prompts user. + * + * @param ServerDTO $server Server to install PHP on + * @param array $info Server information from serverInfo() + * @param array $packageList Package list from package-list playbook + * @return array{status: int, php_version: string, php_default: bool, php_default_prompted: bool, php_extensions: string}|int Returns array with status and values, or int on failure + */ + private function installPhp(ServerDTO $server, array $info, array $packageList): array|int + { + // + // Default extension list + // ---- + + $defaultExtensions = [ + 'bcmath', 'cli', 'common', 'curl', 'fpm', 'gd', 'gmp', + 'igbinary', 'imagick', 'imap', 'intl', 'mbstring', + 'memcached', 'msgpack', 'mysql', 'opcache', 'pgsql', + 'readline', 'redis', 'soap', 'sqlite3', 'swoole', 'xml', 'zip', + ]; + + // + // Extract available PHP versions + // ---- + + if (!isset($packageList['php']) || !is_array($packageList['php']) || empty($packageList['php'])) { + $this->io->error('No PHP versions available in package list'); + + return Command::FAILURE; + } + + $phpVersions = array_keys($packageList['php']); + rsort($phpVersions, SORT_NATURAL); // Newest first + + // + // Extract installed PHP versions + // ---- + + $installedPhpVersions = []; + if (isset($info['php']) && is_array($info['php']) && isset($info['php']['versions']) && is_array($info['php']['versions'])) { + foreach ($info['php']['versions'] as $version) { + // Handle both new format (array with version/extensions) and old format (string) + if (is_array($version) && isset($version['version'])) { + /** @var string $versionStr */ + $versionStr = $version['version']; + $installedPhpVersions[] = $versionStr; + } elseif (is_string($version) || is_numeric($version)) { + $installedPhpVersions[] = (string) $version; + } + } + } + + // + // Prompt for version to install + // ---- + + $defaultVersion = in_array('8.4', $phpVersions) ? '8.4' : $phpVersions[0]; + $phpVersion = (string) $this->io->getOptionOrPrompt( + 'php-version', + fn () => $this->io->promptSelect( + label: 'PHP version:', + options: $phpVersions, + default: $defaultVersion + ) + ); + + // Validate CLI-provided version exists in available versions + if (!in_array($phpVersion, $phpVersions, true)) { + $this->io->error( + "PHP version {$phpVersion} is not available. Available versions: " . implode(', ', $phpVersions) + ); + + return Command::FAILURE; + } + + // + // Select PHP extensions + // ---- + + /** @var array $availableExtensions */ + $availableExtensions = []; + + /** @var array $phpData */ + $phpData = $packageList['php']; + /** @var mixed $versionData */ + $versionData = $phpData[$phpVersion] ?? null; + if (is_array($versionData) && isset($versionData['extensions']) && is_array($versionData['extensions'])) { + /** @var array $extensions */ + $extensions = $versionData['extensions']; + $availableExtensions = $extensions; + } + + if (empty($availableExtensions)) { + $this->io->error("No extensions available for PHP {$phpVersion}"); + + return Command::FAILURE; + } + + // Filter defaults to only those available for this version + $preSelected = array_values(array_intersect($defaultExtensions, $availableExtensions)); + + $selectedExtensions = $this->io->getOptionOrPrompt( + 'php-extensions', + fn () => $this->io->promptMultiselect( + label: 'Select PHP extensions:', + options: $availableExtensions, + default: $preSelected, + scroll: 15 + ) + ); + + // Handle both array (from prompt) and string (from CLI option) + if (is_string($selectedExtensions)) { + $selectedExtensions = array_filter( + array_map(trim(...), explode(',', $selectedExtensions)), + static fn (string $ext): bool => $ext !== '' + ); + } + + if (!is_array($selectedExtensions)) { + $this->io->error('Invalid PHP extensions selection'); + + return Command::FAILURE; + } + + // Validate all selected extensions exist for this PHP version + $unknownExtensions = array_diff($selectedExtensions, $availableExtensions); + if ($unknownExtensions !== []) { + $this->io->error( + 'Unknown PHP extensions for PHP ' . $phpVersion . ': ' . implode(', ', $unknownExtensions) + ); + + return Command::FAILURE; + } + + if ($selectedExtensions === []) { + $this->io->error('At least one extension must be selected'); + + return Command::FAILURE; + } + + // + // Determine if setting as default + // ---- + + $defaultPrompted = false; + + if (count($installedPhpVersions) === 0) { + // First PHP install - automatically set as default + $setAsDefault = true; + } else { + // Check if selected version is already the default + /** @var array{default?: string|int|float}|null $phpInfo */ + $phpInfo = $info['php'] ?? null; + $currentDefault = is_array($phpInfo) ? ($phpInfo['default'] ?? null) : null; + $isAlreadyDefault = $currentDefault !== null && (string) $currentDefault === $phpVersion; + + if ($isAlreadyDefault) { + // Selected version is already default - skip prompt + $setAsDefault = true; + } else { + // PHP already installed but not default - ask user + $defaultPrompted = true; + $setAsDefault = (bool) $this->io->getOptionOrPrompt( + 'php-default', + fn () => $this->io->promptConfirm( + label: "Set PHP {$phpVersion} as default?", + default: false + ) + ); + } + } + + // + // Execute installation playbook + // ---- + + /** @var string $distro */ + $distro = $info['distro']; + /** @var string $permissions */ + $permissions = $info['permissions']; + + $result = $this->executePlaybook( + $server, + 'install-php', + "Installing PHP {$phpVersion}...", + [ + 'DEPLOYER_DISTRO' => $distro, + 'DEPLOYER_PERMS' => $permissions, + 'DEPLOYER_PHP_VERSION' => $phpVersion, + 'DEPLOYER_PHP_SET_DEFAULT' => $setAsDefault ? 'true' : 'false', + 'DEPLOYER_PHP_EXTENSIONS' => implode(',', $selectedExtensions), + ], + true + ); + + if (is_int($result)) { + $this->io->error('PHP installation failed'); + + return Command::FAILURE; + } + + $defaultStatus = $setAsDefault ? ' (set as default)' : ''; + $this->yay("Installed PHP {$phpVersion} successfully{$defaultStatus}"); + + return [ + 'status' => Command::SUCCESS, + 'php_version' => $phpVersion, + 'php_default' => $setAsDefault, + 'php_default_prompted' => $defaultPrompted, + 'php_extensions' => implode(',', $selectedExtensions), + ]; + } + // // HTTP Verification // ---- @@ -186,58 +480,53 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function verifyInstallation(string $url, ?string $deployKey): array { - try { - $client = new Client([ - 'timeout' => 10, - 'http_errors' => false, - ]); - - $response = $client->get($url); - $statusCode = $response->getStatusCode(); - $body = (string) $response->getBody(); + $result = $this->http->verifyUrl($url); - if ($statusCode !== 200) { + if (!$result['success']) { + // Network errors return status_code: 0 + if (0 === $result['status_code']) { return [ 'status' => 'warning', - 'message' => "Demo site returned HTTP {$statusCode} (expected 200)", + 'message' => "Could not connect to demo site: {$result['body']}", 'lines' => [], ]; } - if (!str_contains($body, 'hello, world')) { - return [ - 'status' => 'warning', - 'message' => 'Demo site is responding but content verification failed', - 'lines' => [], - ]; - } - - $nextSteps = [ - 'Next steps:', - ' • Caddy running at ' . $url . '', - ' • Run site:add to deploy your first application', - ]; - - if ($deployKey !== null) { - $nextSteps[] = ' • Add this key to your Git provider (GitHub, GitLab, etc.) to enable deployments:'; - $nextSteps[] = ''; - $nextSteps[] = '' . $deployKey . ''; - } - - $nextSteps[] = ''; - + // HTTP protocol errors (got response but wrong status code) return [ - 'status' => 'success', - 'message' => 'Server installation completed successfully', - 'lines' => $nextSteps, + 'status' => 'warning', + 'message' => "Demo site returned HTTP {$result['status_code']} (expected 200)", + 'lines' => [], ]; - } catch (\Throwable $e) { + } + + if (!str_contains($result['body'], 'hello, world')) { return [ 'status' => 'warning', - 'message' => 'Could not verify demo site: ' . $e->getMessage(), + 'message' => 'Demo site is responding but content verification failed', 'lines' => [], ]; } + + $nextSteps = [ + 'Next steps:', + ' • Caddy running at ' . $url . '', + ' • Run site:add to deploy your first application', + ]; + + if ($deployKey !== null) { + $nextSteps[] = ' • Add this key to your Git provider (GitHub, GitLab, etc.) to enable deployments:'; + $nextSteps[] = ''; + $nextSteps[] = '' . $deployKey . ''; + } + + $nextSteps[] = ''; + + return [ + 'status' => 'success', + 'message' => 'Server installation completed successfully', + 'lines' => $nextSteps, + ]; } } diff --git a/app/Console/Server/ServerInstallPhpCommand.php b/app/Console/Server/ServerInstallPhpCommand.php deleted file mode 100644 index 4ad6d73b..00000000 --- a/app/Console/Server/ServerInstallPhpCommand.php +++ /dev/null @@ -1,97 +0,0 @@ -addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name'); - $this->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version to install'); - $this->addOption('php-default', null, InputOption::VALUE_NONE, 'Set as default PHP version'); - } - - // ---- - // Execution - // ---- - - protected function execute(InputInterface $input, OutputInterface $output): int - { - parent::execute($input, $output); - - $this->heading('Install PHP'); - - // - // Select server & display details - // ---- - - $server = $this->selectServer(); - - if (is_int($server)) { - return $server; - } - - $this->displayServerDeets($server); - - // - // Get server info (verifies SSH connection and validates distribution & permissions) - // ---- - - $info = $this->serverInfo($server); - - if (is_int($info)) { - return $info; - } - - // - // Install PHP - // ---- - - $phpResult = $this->installPhp($server, $info); - - if (is_int($phpResult)) { - return $phpResult; - } - - /** @var array{status: int, php_version: string, php_default: bool} $phpResult */ - $phpVersion = $phpResult['php_version']; - $phpDefault = $phpResult['php_default']; - - // - // Show command replay - // ---- - - $this->showCommandReplay('server:install:php', [ - 'server' => $server->name, - 'php-version' => $phpVersion, - 'php-default' => $phpDefault, - ]); - - return Command::SUCCESS; - } - -} diff --git a/app/Contracts/BaseCommand.php b/app/Contracts/BaseCommand.php index c8d2dff3..7d89d00f 100644 --- a/app/Contracts/BaseCommand.php +++ b/app/Contracts/BaseCommand.php @@ -11,6 +11,7 @@ use Bigpixelrocket\DeployerPHP\Services\EnvService; use Bigpixelrocket\DeployerPHP\Services\FilesystemService; use Bigpixelrocket\DeployerPHP\Services\GitService; +use Bigpixelrocket\DeployerPHP\Services\HttpService; use Bigpixelrocket\DeployerPHP\Services\InventoryService; use Bigpixelrocket\DeployerPHP\Services\IOService; use Bigpixelrocket\DeployerPHP\Services\ProcessService; @@ -43,6 +44,7 @@ public function __construct( protected readonly EnvService $env, protected readonly FilesystemService $fs, protected readonly GitService $git, + protected readonly HttpService $http, protected readonly InventoryService $inventory, protected readonly IOService $io, protected readonly ProcessService $proc, diff --git a/app/Services/HttpService.php b/app/Services/HttpService.php new file mode 100644 index 00000000..10e19138 --- /dev/null +++ b/app/Services/HttpService.php @@ -0,0 +1,50 @@ +client = new Client([ + 'timeout' => 10, + 'http_errors' => false, + ]); + } + + /** + * Verify URL responds with expected status and content. + * + * @return array{success: bool, status_code: int, body: string} + */ + public function verifyUrl(string $url): array + { + try { + $response = $this->client->get($url); + + return [ + 'success' => $response->getStatusCode() === 200, + 'status_code' => $response->getStatusCode(), + 'body' => (string) $response->getBody(), + ]; + } catch (GuzzleException $e) { + return [ + 'success' => false, + 'status_code' => 0, + 'body' => $e->getMessage(), + ]; + } + } +} diff --git a/app/SymfonyApp.php b/app/SymfonyApp.php index 690ea658..b915c754 100644 --- a/app/SymfonyApp.php +++ b/app/SymfonyApp.php @@ -12,7 +12,6 @@ use Bigpixelrocket\DeployerPHP\Console\Server\ServerDeleteCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerInfoCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerInstallCommand; -use Bigpixelrocket\DeployerPHP\Console\Server\ServerInstallPhpCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerListCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerLogsCommand; use Bigpixelrocket\DeployerPHP\Console\Server\ServerProvisionDigitalOceanCommand; @@ -145,7 +144,6 @@ private function registerCommands(): void ServerListCommand::class, ServerInfoCommand::class, ServerInstallCommand::class, - ServerInstallPhpCommand::class, ServerLogsCommand::class, ServerRunCommand::class, diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index 942c9140..d6b3a48b 100644 --- a/app/Traits/ServersTrait.php +++ b/app/Traits/ServersTrait.php @@ -255,9 +255,37 @@ protected function displayServerInfo(array $info): void $versions = $info['php']['versions']; $defaultVersion = $info['php']['default'] ?? null; - foreach ($versions as $version) { - if (is_string($version) || is_numeric($version)) { + foreach ($versions as $versionData) { + // Handle both old format (string) and new format (array with version/extensions) + if (is_array($versionData) && isset($versionData['version'])) { + /** @var string|int|float */ + $version = $versionData['version']; $versionStr = (string) $version; + $extensions = $versionData['extensions'] ?? []; + + // Build version line with extensions + $isDefault = false; + if ($defaultVersion !== null && (is_string($defaultVersion) || is_numeric($defaultVersion))) { + /** @var string|int|float $defaultVersion */ + $isDefault = $versionStr === (string) $defaultVersion; + } + + $versionLine = "PHP {$versionStr}"; + if ($isDefault) { + $versionLine .= ' (default)'; + } + + // Add extensions if available + if (is_array($extensions) && count($extensions) > 0) { + $extCount = count($extensions); + $extList = implode(', ', $extensions); + $versionLine .= " with {$extCount} extensions: {$extList}"; + } + + $phpItems[] = $versionLine; + } elseif (is_string($versionData) || is_numeric($versionData)) { + // Fallback for old format (simple string/numeric version) + $versionStr = (string) $versionData; if ($defaultVersion !== null && (is_string($defaultVersion) || is_numeric($defaultVersion))) { /** @var string|int|float $defaultVersion */ $isDefault = $versionStr === (string) $defaultVersion; @@ -392,102 +420,6 @@ private function formatUptime(int $seconds): string return "{$days}d {$hours}h"; } - /** - * Install PHP on a server. - * - * Prompts for PHP version selection and handles installation via playbook. - * Automatically sets first PHP install as default, otherwise prompts user. - * - * @param ServerDTO $server Server to install PHP on - * @param array $info Server information from serverInfo() - * @return array{status: int, php_version: string, php_default: bool}|int Returns array with status and values, or int on failure - */ - protected function installPhp(ServerDTO $server, array $info): array|int - { - $phpVersions = ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; - - // - // Extract installed PHP versions - // ---- - - $installedPhpVersions = []; - if (isset($info['php']) && is_array($info['php']) && isset($info['php']['versions']) && is_array($info['php']['versions'])) { - foreach ($info['php']['versions'] as $version) { - if (is_string($version) || is_numeric($version)) { - $installedPhpVersions[] = (string) $version; - } - } - } - - // - // Prompt for version to install - // ---- - - $phpVersion = (string) $this->io->getOptionOrPrompt( - 'php-version', - fn () => $this->io->promptSelect( - label: 'PHP version:', - options: $phpVersions, - default: '8.4' - ) - ); - - // - // Determine if setting as default - // ---- - - if (count($installedPhpVersions) === 0) { - // First PHP install - automatically set as default - $setAsDefault = true; - } else { - // PHP already installed - ask user - $setAsDefault = (bool) $this->io->getOptionOrPrompt( - 'php-default', - fn () => $this->io->promptConfirm( - label: "Set PHP {$phpVersion} as default?", - default: false - ) - ); - } - - // - // Execute installation playbook - // ---- - - /** @var string $distro */ - $distro = $info['distro']; - /** @var string $permissions */ - $permissions = $info['permissions']; - - $result = $this->executePlaybook( - $server, - 'server-install-php', - "Installing PHP {$phpVersion}...", - [ - 'DEPLOYER_DISTRO' => $distro, - 'DEPLOYER_PERMS' => $permissions, - 'DEPLOYER_PHP_VERSION' => $phpVersion, - 'DEPLOYER_PHP_SET_DEFAULT' => $setAsDefault ? 'true' : 'false', - ], - true - ); - - if (is_int($result)) { - $this->io->error('PHP installation failed'); - - return Command::FAILURE; - } - - $defaultStatus = $setAsDefault ? ' (set as default)' : ''; - $this->yay("PHP {$phpVersion} installed successfully{$defaultStatus}"); - - return [ - 'status' => Command::SUCCESS, - 'php_version' => $phpVersion, - 'php_default' => $setAsDefault, - ]; - } - // // UI // ---- diff --git a/playbooks/demo-site.sh b/playbooks/demo-site.sh index 03cd6fdd..81954200 100644 --- a/playbooks/demo-site.sh +++ b/playbooks/demo-site.sh @@ -11,6 +11,7 @@ # # Required Environment Variables: # DEPLOYER_OUTPUT_FILE - Output file path +# DEPLOYER_DISTRO - Exact distribution: ubuntu|debian # DEPLOYER_PERMS - Permissions: root|sudo # # Returns YAML with: @@ -24,6 +25,7 @@ set -o pipefail export DEBIAN_FRONTEND=noninteractive [[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 +[[ -z $DEPLOYER_DISTRO ]] && echo "Error: DEPLOYER_DISTRO required" && exit 1 [[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 export DEPLOYER_PERMS @@ -53,6 +55,29 @@ require_deployer_user() { fi } +# +# Verify required services are installed + +require_services() { + if ! command -v caddy > /dev/null 2>&1; then + echo "Error: Caddy not found. Run server:install to install base packages first." >&2 + exit 1 + fi + + # Verify PHP is available (detect_php_default returns empty string if not found) + local php_version + php_version=$(detect_php_default) + if [[ -z $php_version ]]; then + echo "Error: No PHP installation found. Run server:install to install PHP first." >&2 + exit 1 + fi + + if ! run_cmd test -d /etc/caddy/conf.d/sites; then + echo "Error: Caddy site directory missing. Run server:install to configure Caddy first." >&2 + exit 1 + fi +} + # # Demo Site Setup # ---- @@ -61,7 +86,7 @@ require_deployer_user() { # Create demo site directory structure and files setup_demo_site() { - echo "✓ Setting up demo site..." + echo "→ Setting up demo site..." # Create directory structure if ! run_cmd test -d /home/deployer/demo/public; then @@ -118,7 +143,7 @@ setup_demo_site() { # Configure Caddy for demo site configure_demo_site() { - echo "✓ Configuring demo site..." + echo "→ Configuring demo site..." # Detect default PHP version local php_version @@ -129,7 +154,7 @@ configure_demo_site() { exit 1 fi - echo "✓ Using PHP ${php_version} (default)" + echo "→ Using PHP ${php_version} (default)" # PHP-FPM socket path (debian family) local php_fpm_socket="/run/php/php${php_version}-fpm.sock" @@ -163,8 +188,11 @@ configure_demo_site() { format json } - # This single line handles everything: PHP files, index.php routing, and static files - php_fastcgi unix/${php_fpm_socket} + # Serve PHP files through FPM + php_fastcgi unix//${php_fpm_socket} + + # Serve static files directly (more efficient than passing through PHP-FPM) + file_server } EOF echo "Error: Failed to create demo.caddy" >&2 @@ -190,6 +218,13 @@ configure_demo_site() { exit 1 fi fi + + # Restart PHP-FPM to ensure proper socket communication with Caddy + if systemctl is-active --quiet "php${php_version}-fpm" 2> /dev/null; then + if ! run_cmd systemctl restart "php${php_version}-fpm"; then + echo "Warning: Failed to restart PHP-FPM service" + fi + fi } # ---- @@ -199,6 +234,7 @@ configure_demo_site() { main() { # Execute setup tasks require_deployer_user + require_services setup_demo_site configure_demo_site diff --git a/playbooks/helpers.sh b/playbooks/helpers.sh index cf7d1f98..fa44faf5 100644 --- a/playbooks/helpers.sh +++ b/playbooks/helpers.sh @@ -71,7 +71,7 @@ wait_for_dpkg_lock() { || fuser /var/lib/dpkg/lock > /dev/null 2>&1 \ || fuser /var/lib/apt/lists/lock > /dev/null 2>&1; then lock_found=true - echo "✓ Waiting for package manager lock to be released..." + echo "Waiting for package manager lock to be released..." sleep 2 waited=$((waited + 2)) else @@ -113,7 +113,7 @@ apt_get_with_retry() { # Only retry on lock-related errors if echo "$output" | grep -qE 'Could not get lock|dpkg.*lock|Unable to acquire'; then if ((attempt < max_attempts)); then - echo "✓ Package manager locked, waiting ${wait_time}s before retry (attempt ${attempt}/${max_attempts})..." + echo "Package manager locked, waiting ${wait_time}s before retry (attempt ${attempt}/${max_attempts})..." sleep "$wait_time" wait_time=$((wait_time + 5)) attempt=$((attempt + 1)) diff --git a/playbooks/install-base.sh b/playbooks/install-base.sh new file mode 100644 index 00000000..5f968f74 --- /dev/null +++ b/playbooks/install-base.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash + +# +# Server Installation Playbook - Ubuntu/Debian Only +# +# Install Caddy, Git, and configure base server +# ---- +# +# This playbook only supports Ubuntu and Debian distributions (debian family). +# Both distributions use apt package manager and follow debian conventions. +# +# Required Environment Variables: +# DEPLOYER_OUTPUT_FILE - Output file path +# DEPLOYER_DISTRO - Exact distribution: ubuntu|debian +# DEPLOYER_PERMS - Permissions: root|sudo +# +# Returns YAML with: +# - status: success +# + +set -o pipefail +export DEBIAN_FRONTEND=noninteractive + +[[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 +[[ -z $DEPLOYER_DISTRO ]] && echo "Error: DEPLOYER_DISTRO required" && exit 1 +[[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 +export DEPLOYER_PERMS + +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" + +# ---- +# Installation Functions +# ---- + +# +# Install packages +# ---- + +install_packages() { + echo "→ Installing packages..." + + local common_packages=(curl zip unzip caddy git rsync) + local distro_packages + + case $DEPLOYER_DISTRO in + ubuntu) + distro_packages=(software-properties-common) + if ! apt_get_with_retry install -y "${common_packages[@]}" "${distro_packages[@]}"; then + echo "Error: Failed to install packages" >&2 + exit 1 + fi + ;; + debian) + distro_packages=(apt-transport-https lsb-release ca-certificates) + if ! apt_get_with_retry install -y "${common_packages[@]}" "${distro_packages[@]}"; then + echo "Error: Failed to install packages" >&2 + exit 1 + fi + ;; + esac +} + +# +# Base Caddy config +# ---- + +config_caddy() { + echo "→ Setting up Caddy base config..." + + # + # Create directory structure + + if ! run_cmd mkdir -p /etc/caddy/conf.d/sites; then + echo "Error: Failed to create Caddy config directories" >&2 + exit 1 + fi + + # + # Create main Caddyfile with global settings and imports + + # Check if our custom config is already in place + if ! grep -q "import conf.d/localhost.caddy" /etc/caddy/Caddyfile 2> /dev/null; then + echo "→ Creating Caddyfile with custom configuration..." + if ! run_cmd tee /etc/caddy/Caddyfile > /dev/null <<- 'EOF'; then + { + metrics + + log { + output file /var/log/caddy/access.log + format json + } + } + + # Import localhost-only endpoints (monitoring, status pages) + import conf.d/localhost.caddy + + # Import all site configurations + import conf.d/sites/*.caddy + EOF + echo "Error: Failed to create main Caddyfile" >&2 + exit 1 + fi + fi + + # Create localhost.caddy - monitoring endpoints only accessible via localhost + # (PHP-FPM status endpoint will be added by PHP installation playbook) + if ! run_cmd test -f /etc/caddy/conf.d/localhost.caddy; then + if ! run_cmd tee /etc/caddy/conf.d/localhost.caddy > /dev/null <<- 'EOF'; then + # PHP-FPM status endpoints - localhost only (not accessible from internet) + http://localhost:9001 { + #### DEPLOYER-PHP CONFIG, WARRANTY VOID IF REMOVED :) #### + } + EOF + echo "Error: Failed to create localhost.caddy" >&2 + exit 1 + fi + fi + + # Reload Caddy to apply configuration changes + if systemctl is-active --quiet caddy 2> /dev/null; then + echo "→ Reloading Caddy configuration..." + if ! run_cmd systemctl reload caddy 2> /dev/null; then + echo "Warning: Failed to reload Caddy configuration" + fi + fi +} + +# ---- +# Main Execution +# ---- + +main() { + # Execute installation tasks + install_packages + config_caddy + + # Write output YAML + if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then + status: success + EOF + echo "Error: Failed to write output file" >&2 + exit 1 + fi +} + +main "$@" diff --git a/playbooks/install-bun.sh b/playbooks/install-bun.sh new file mode 100644 index 00000000..49ee2a4e --- /dev/null +++ b/playbooks/install-bun.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + +# +# Bun Installation Playbook +# +# Install Bun JavaScript runtime and toolkit +# ---- +# +# This playbook installs Bun system-wide to /usr/local. +# Bun installation is distribution-agnostic (uses official installer). +# +# Required Environment Variables: +# DEPLOYER_OUTPUT_FILE - Output file path +# DEPLOYER_PERMS - Permissions: root|sudo +# +# Returns YAML with: +# - status: success +# + +set -o pipefail +export DEBIAN_FRONTEND=noninteractive + +[[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 +[[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 +export DEPLOYER_PERMS + +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" + +# ---- +# Main Execution +# ---- + +main() { + if command -v bun > /dev/null 2>&1; then + echo "Bun is already installed (run 'bun upgrade' manually to upgrade if needed)" + else + echo "→ Installing Bun..." + # Install Bun system-wide to /usr/local + if ! curl -fsSL https://bun.sh/install | run_cmd env BUN_INSTALL=/usr/local bash; then + echo "Error: Failed to install Bun" >&2 + exit 1 + fi + fi + + # Write output YAML + if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then + status: success + EOF + echo "Error: Failed to write output file" >&2 + exit 1 + fi +} + +main "$@" diff --git a/playbooks/install-deployer.sh b/playbooks/install-deployer.sh new file mode 100644 index 00000000..cd328b08 --- /dev/null +++ b/playbooks/install-deployer.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash + +# +# Deployer User Setup Playbook +# +# Creates deployer user, generates SSH keys, and configures permissions +# ---- +# +# This playbook handles all deployer user-related configuration including: +# - User creation with home directory +# - SSH key pair generation for git deployments +# - Group memberships (adding caddy and www-data to deployer group) +# - Directory permissions for deployer home +# +# Required Environment Variables: +# DEPLOYER_OUTPUT_FILE - Output file path +# DEPLOYER_PERMS - Permissions: root|sudo +# DEPLOYER_SERVER_NAME - Server name for deploy key generation +# +# Returns YAML with: +# - status: success +# - deploy_public_key: public key for git deployments +# + +set -o pipefail +export DEBIAN_FRONTEND=noninteractive + +[[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 +[[ -z $DEPLOYER_DISTRO ]] && echo "Error: DEPLOYER_DISTRO required" && exit 1 +[[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 +[[ -z $DEPLOYER_SERVER_NAME ]] && echo "Error: DEPLOYER_SERVER_NAME required" && exit 1 +export DEPLOYER_PERMS + +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" + +# ---- +# Setup Functions +# ---- + +# +# Setup deployer user +# ---- + +setup_deployer() { + local deployer_home=$1 + + # Ensure home directory has correct permissions (default is usually 755) + if ! run_cmd chmod 750 "$deployer_home"; then + echo "Error: Failed to set permissions on deployer home directory" >&2 + exit 1 + fi + + # + # Configure group memberships + # ---- + + # Add caddy user to deployer group for file access + if id -u caddy > /dev/null 2>&1; then + if ! id -nG caddy 2> /dev/null | grep -qw deployer; then + echo "→ Adding caddy user to deployer group..." + if ! run_cmd usermod -aG deployer caddy; then + echo "Error: Failed to add caddy to deployer group" >&2 + exit 1 + fi + + if systemctl is-active --quiet caddy 2> /dev/null; then + echo "→ Restarting Caddy to apply group membership..." + if ! run_cmd systemctl restart caddy; then + echo "Error: Failed to restart Caddy" >&2 + exit 1 + fi + fi + fi + fi + + # Add www-data (PHP-FPM user) to deployer group for file access + if id -u www-data > /dev/null 2>&1; then + if ! id -nG www-data 2> /dev/null | grep -qw deployer; then + echo "→ Adding www-data user to deployer group..." + if ! run_cmd usermod -aG deployer www-data; then + echo "Error: Failed to add www-data to deployer group" >&2 + exit 1 + fi + + # Restart all active PHP-FPM services to apply group membership + local fpm_services + fpm_services=$(systemctl list-units --type=service --state=active 'php*-fpm.service' --no-legend 2> /dev/null | awk '{print $1}') + + if [[ -n $fpm_services ]]; then + echo "→ Restarting PHP-FPM services to apply group membership..." + while IFS= read -r service; do + if ! run_cmd systemctl restart "$service"; then + echo "Warning: Failed to restart $service" + fi + done <<< "$fpm_services" + fi + fi + fi + + # + # Setup SSH deploy key + # ---- + + local deployer_ssh_dir="${deployer_home}/.ssh" + local private_key="${deployer_ssh_dir}/id_ed25519" + local public_key="${deployer_ssh_dir}/id_ed25519.pub" + + if ! run_cmd test -d "$deployer_ssh_dir"; then + echo "→ Creating .ssh directory..." + if ! run_cmd mkdir -p "$deployer_ssh_dir"; then + echo "Error: Failed to create .ssh directory" >&2 + exit 1 + fi + fi + + if ! run_cmd test -f "$private_key"; then + echo "→ Generating SSH key pair..." + if ! run_cmd ssh-keygen -t ed25519 -C "deployer@${DEPLOYER_SERVER_NAME}" -f "$private_key" -N ""; then + echo "Error: Failed to generate SSH key pair" >&2 + exit 1 + fi + else + echo "→ SSH key pair already exists" + fi + + # Set ownership and permissions + if ! run_cmd chown -R deployer:deployer "$deployer_ssh_dir"; then + echo "Error: Failed to set ownership on .ssh directory" >&2 + exit 1 + fi + + if ! run_cmd chmod 700 "$deployer_ssh_dir"; then + echo "Error: Failed to set permissions on .ssh directory" >&2 + exit 1 + fi + + if ! run_cmd chmod 600 "$private_key"; then + echo "Error: Failed to set permissions on private key" >&2 + exit 1 + fi + + if ! run_cmd chmod 644 "$public_key"; then + echo "Error: Failed to set permissions on public key" >&2 + exit 1 + fi +} + +# +# Ensure proper permissions on deploy directories +# ---- + +setup_deploy_directories() { + local deployer_home=$1 + + if ! run_cmd test -d "$deployer_home"; then + echo "Error: Deployer home directory missing" >&2 + exit 1 + fi +} + +# ---- +# Main Execution +# ---- + +main() { + local deploy_public_key + local deployer_home + + # Create deployer user if it doesn't exist + if ! id -u deployer > /dev/null 2>&1; then + echo "→ Creating deployer user..." + if ! run_cmd useradd -m -s /bin/bash deployer; then + echo "Error: Failed to create deployer user" >&2 + exit 1 + fi + fi + + # Discover deployer home directory (must be after user creation) + deployer_home=$(getent passwd deployer | cut -d: -f6) + if [[ -z $deployer_home ]]; then + echo "Error: Unable to determine deployer home directory" >&2 + exit 1 + fi + + # Execute deployer setup tasks + setup_deployer "$deployer_home" + setup_deploy_directories "$deployer_home" + + # Get deploy public key + if ! deploy_public_key=$(run_cmd cat "${deployer_home}/.ssh/id_ed25519.pub" 2>&1); then + echo "Error: Failed to read deploy public key at ${deployer_home}/.ssh/id_ed25519.pub" >&2 + exit 1 + fi + + # Write output YAML + if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then + status: success + deploy_public_key: $deploy_public_key + EOF + echo "Error: Failed to write output file" >&2 + exit 1 + fi +} + +main "$@" diff --git a/playbooks/server-install-php.sh b/playbooks/install-php.sh similarity index 50% rename from playbooks/server-install-php.sh rename to playbooks/install-php.sh index 19120db9..5295d414 100644 --- a/playbooks/server-install-php.sh +++ b/playbooks/install-php.sh @@ -3,9 +3,18 @@ # # PHP Installation Playbook - Ubuntu/Debian Only # -# Install specified PHP version with FPM and common extensions +# Install specified PHP version with user-selected extensions # ---- # +# This playbook handles PHP installation and configuration including: +# - Package installation (PHP core + selected extensions) +# - PHP-FPM service configuration and socket permissions +# - Optional system default version setup +# - Caddy configuration updates for PHP-FPM endpoints +# +# Note: User and group management (www-data to deployer group) is handled +# by the install-deployer.sh playbook which runs after all services. +# # This playbook only supports Ubuntu and Debian distributions (debian family). # Both distributions use apt package manager and follow debian conventions. # @@ -15,13 +24,10 @@ # DEPLOYER_PERMS - Permissions: root|sudo # DEPLOYER_PHP_VERSION - PHP version to install (e.g., 8.4, 8.3, 7.4) # DEPLOYER_PHP_SET_DEFAULT - Set as system default: true|false +# DEPLOYER_PHP_EXTENSIONS - Comma-separated list of extensions to install (e.g., cli,fpm,mysql) # # Returns YAML with: # - status: success -# - php_version: installed PHP version -# - is_default: whether this version is set as system default -# - fpm_socket_path: path to PHP-FPM socket -# - tasks_completed: list of completed tasks # set -o pipefail @@ -32,6 +38,7 @@ export DEBIAN_FRONTEND=noninteractive [[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 [[ -z $DEPLOYER_PHP_VERSION ]] && echo "Error: DEPLOYER_PHP_VERSION required" && exit 1 [[ -z $DEPLOYER_PHP_SET_DEFAULT ]] && echo "Error: DEPLOYER_PHP_SET_DEFAULT required" && exit 1 +[[ -z $DEPLOYER_PHP_EXTENSIONS ]] && echo "Error: DEPLOYER_PHP_EXTENSIONS required" && exit 1 export DEPLOYER_PERMS # Shared helpers are automatically inlined when executing playbooks remotely @@ -41,47 +48,6 @@ export DEPLOYER_PERMS # Installation Functions # ---- -# -# Repository Setup -# ---- - -# -# Setup PHP repository - -setup_php_repository() { - echo "✓ Setting up PHP repository..." - - case $DEPLOYER_DISTRO in - ubuntu) - # PHP PPA (Ubuntu only) - if ! grep -qr "ondrej/php" /etc/apt/sources.list /etc/apt/sources.list.d/ 2> /dev/null; then - if ! run_cmd env DEBIAN_FRONTEND=noninteractive add-apt-repository -y ppa:ondrej/php 2>&1; then - echo "Error: Failed to add PHP PPA" >&2 - exit 1 - fi - fi - ;; - debian) - # Sury PHP repository (Debian only) - if ! [[ -f /usr/share/keyrings/php-sury-archive-keyring.gpg ]]; then - if ! curl -fsSL 'https://packages.sury.org/php/apt.gpg' | run_cmd gpg --batch --yes --dearmor -o /usr/share/keyrings/php-sury-archive-keyring.gpg; then - echo "Error: Failed to add Sury PHP GPG key" >&2 - exit 1 - fi - fi - - if ! [[ -f /etc/apt/sources.list.d/php-sury.list ]]; then - local debian_codename - debian_codename=$(lsb_release -sc) - if ! echo "deb [signed-by=/usr/share/keyrings/php-sury-archive-keyring.gpg] https://packages.sury.org/php/ ${debian_codename} main" | run_cmd tee /etc/apt/sources.list.d/php-sury.list > /dev/null; then - echo "Error: Failed to add Sury PHP repository" >&2 - exit 1 - fi - fi - ;; - esac -} - # # Package Installation # ---- @@ -90,41 +56,19 @@ setup_php_repository() { # Install PHP packages for specified version install_php_packages() { - echo "✓ Installing PHP ${DEPLOYER_PHP_VERSION}..." + echo "→ Installing PHP ${DEPLOYER_PHP_VERSION}..." - # Update package lists - echo "✓ Updating package lists..." - if ! apt_get_with_retry update -q; then - echo "Error: Failed to update package lists" >&2 - exit 1 - fi + # Parse comma-separated extensions + IFS=',' read -ra extensions <<< "$DEPLOYER_PHP_EXTENSIONS" + local packages=() - # Install PHP packages - if ! apt_get_with_retry install -y -q \ - php${DEPLOYER_PHP_VERSION}-bcmath \ - php${DEPLOYER_PHP_VERSION}-cli \ - php${DEPLOYER_PHP_VERSION}-common \ - php${DEPLOYER_PHP_VERSION}-curl \ - php${DEPLOYER_PHP_VERSION}-fpm \ - php${DEPLOYER_PHP_VERSION}-gd \ - php${DEPLOYER_PHP_VERSION}-gmp \ - php${DEPLOYER_PHP_VERSION}-igbinary \ - php${DEPLOYER_PHP_VERSION}-imagick \ - php${DEPLOYER_PHP_VERSION}-imap \ - php${DEPLOYER_PHP_VERSION}-intl \ - php${DEPLOYER_PHP_VERSION}-mbstring \ - php${DEPLOYER_PHP_VERSION}-memcached \ - php${DEPLOYER_PHP_VERSION}-msgpack \ - php${DEPLOYER_PHP_VERSION}-mysql \ - php${DEPLOYER_PHP_VERSION}-opcache \ - php${DEPLOYER_PHP_VERSION}-pgsql \ - php${DEPLOYER_PHP_VERSION}-readline \ - php${DEPLOYER_PHP_VERSION}-redis \ - php${DEPLOYER_PHP_VERSION}-soap \ - php${DEPLOYER_PHP_VERSION}-sqlite3 \ - php${DEPLOYER_PHP_VERSION}-swoole \ - php${DEPLOYER_PHP_VERSION}-xml \ - php${DEPLOYER_PHP_VERSION}-zip 2>&1; then + for ext in "${extensions[@]}"; do + ext=$(echo "$ext" | xargs) # trim whitespace + packages+=("php${DEPLOYER_PHP_VERSION}-${ext}") + done + + # Install selected packages + if ! apt_get_with_retry install -y "${packages[@]}" 2>&1; then echo "Error: Failed to install PHP ${DEPLOYER_PHP_VERSION} packages" >&2 exit 1 fi @@ -138,7 +82,7 @@ install_php_packages() { # Configure PHP-FPM for the installed version configure_php_fpm() { - echo "✓ Configuring PHP-FPM..." + echo "→ Configuring PHP-FPM..." local pool_config="/etc/php/${DEPLOYER_PHP_VERSION}/fpm/pool.d/www.conf" @@ -162,49 +106,18 @@ configure_php_fpm() { exit 1 fi - # Enable and start PHP-FPM service + # Enable PHP-FPM service if ! systemctl is-enabled --quiet php${DEPLOYER_PHP_VERSION}-fpm 2> /dev/null; then if ! run_cmd systemctl enable --quiet php${DEPLOYER_PHP_VERSION}-fpm; then echo "Error: Failed to enable PHP-FPM service" >&2 exit 1 fi fi - if ! systemctl is-active --quiet php${DEPLOYER_PHP_VERSION}-fpm 2> /dev/null; then - if ! run_cmd systemctl start php${DEPLOYER_PHP_VERSION}-fpm; then - echo "Error: Failed to start PHP-FPM service" >&2 - exit 1 - fi - fi -} - -# -# User Group Configuration -# ---- -# -# Configure PHP-FPM user group membership for file access - -configure_php_user_groups() { - # Add www-data (PHP-FPM user) to deployer group so it can access files - if id -u www-data > /dev/null 2>&1; then - if ! id -nG www-data 2> /dev/null | grep -qw deployer; then - echo "✓ Adding www-data user to deployer group..." - if ! run_cmd usermod -aG deployer www-data; then - echo "Error: Failed to add www-data to deployer group" >&2 - exit 1 - fi - - # Restart PHP-FPM so it picks up the new group membership - if systemctl is-active --quiet php${DEPLOYER_PHP_VERSION}-fpm 2> /dev/null; then - echo "✓ Restarting PHP-FPM to apply group membership..." - if ! run_cmd systemctl restart php${DEPLOYER_PHP_VERSION}-fpm; then - echo "Error: Failed to restart PHP-FPM" >&2 - exit 1 - fi - fi - fi - else - echo "Warning: PHP-FPM user 'www-data' not found, skipping group assignment" + # Restart PHP-FPM to apply configuration changes (idempotent - starts if not running) + if ! run_cmd systemctl restart php${DEPLOYER_PHP_VERSION}-fpm; then + echo "Error: Failed to restart PHP-FPM service" >&2 + exit 1 fi } @@ -220,21 +133,13 @@ set_as_default() { return 0 fi - echo "✓ Setting PHP ${DEPLOYER_PHP_VERSION} as system default..." + echo "→ Setting PHP ${DEPLOYER_PHP_VERSION} as system default..." # Set alternatives for php binaries if command -v update-alternatives > /dev/null 2>&1; then - if run_cmd update-alternatives --set php /usr/bin/php${DEPLOYER_PHP_VERSION} 2> /dev/null; then - echo "✓ Set php alternative" - fi - - if run_cmd update-alternatives --set php-config /usr/bin/php-config${DEPLOYER_PHP_VERSION} 2> /dev/null; then - echo "✓ Set php-config alternative" - fi - - if run_cmd update-alternatives --set phpize /usr/bin/phpize${DEPLOYER_PHP_VERSION} 2> /dev/null; then - echo "✓ Set phpize alternative" - fi + run_cmd update-alternatives --set php /usr/bin/php${DEPLOYER_PHP_VERSION} 2> /dev/null + run_cmd update-alternatives --set php-config /usr/bin/php-config${DEPLOYER_PHP_VERSION} 2> /dev/null + run_cmd update-alternatives --set phpize /usr/bin/phpize${DEPLOYER_PHP_VERSION} 2> /dev/null fi } @@ -251,14 +156,13 @@ update_caddy_config() { return 0 fi - echo "✓ Updating Caddy localhost configuration..." - # Check if this PHP version's endpoint already exists if grep -q "handle_path /php${DEPLOYER_PHP_VERSION}/" /etc/caddy/conf.d/localhost.caddy 2> /dev/null; then - echo "✓ PHP ${DEPLOYER_PHP_VERSION} endpoint already configured" return 0 fi + echo "→ Updating Caddy localhost configuration..." + # Check if the marker exists (file should be created by server-install.sh) if ! grep -q "#### DEPLOYER-PHP CONFIG, WARRANTY VOID IF REMOVED :) ####" /etc/caddy/conf.d/localhost.caddy 2> /dev/null; then echo "Error: localhost.caddy marker not found. File may have been modified manually." >&2 @@ -320,38 +224,15 @@ update_caddy_config() { # ---- main() { - local fpm_socket_path="/run/php/php${DEPLOYER_PHP_VERSION}-fpm.sock" - local is_default="false" - # Execute installation tasks - setup_php_repository install_php_packages configure_php_fpm - configure_php_user_groups set_as_default update_caddy_config - if [[ $DEPLOYER_PHP_SET_DEFAULT == 'true' ]]; then - is_default="true" - fi - - # Get actual PHP version - local php_version - php_version=$(php${DEPLOYER_PHP_VERSION} -r "echo PHP_VERSION;" 2> /dev/null || echo "$DEPLOYER_PHP_VERSION") - # Write output YAML if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then status: success - php_version: $php_version - is_default: $is_default - fpm_socket_path: $fpm_socket_path - tasks_completed: - - setup_php_repository - - install_php_packages - - configure_php_fpm - - configure_php_user_groups - - set_as_default - - update_caddy_config EOF echo "Error: Failed to write output file" >&2 exit 1 diff --git a/playbooks/package-list.sh b/playbooks/package-list.sh new file mode 100644 index 00000000..eb1f9b4c --- /dev/null +++ b/playbooks/package-list.sh @@ -0,0 +1,286 @@ +#!/usr/bin/env bash + +# +# Package List Playbook +# +# Update package lists and configure repositories +# ---- +# +# Required Environment Variables: +# DEPLOYER_OUTPUT_FILE - Output file path +# DEPLOYER_DISTRO - Exact distribution: ubuntu|debian +# DEPLOYER_PERMS - Permissions: root|sudo +# +# Optional Environment Variables: +# DEPLOYER_GATHER_PHP - Gather PHP versions and extensions: true|false (default: false) +# +# Returns YAML with: +# - status: success +# - repos_configured: true +# - php: (only if DEPLOYER_GATHER_PHP=true) nested structure with versions and extensions +# + +set -o pipefail +export DEBIAN_FRONTEND=noninteractive + +[[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 +[[ -z $DEPLOYER_DISTRO ]] && echo "Error: DEPLOYER_DISTRO required" && exit 1 +[[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 +export DEPLOYER_PERMS + +# Shared helpers are automatically inlined when executing playbooks remotely +# source "$(dirname "$0")/helpers.sh" + +# ---- +# Helper Functions +# ---- + +# +# Smart apt update with timestamp-based throttling +# +# Arguments: +# $1 - force (optional): true to bypass throttling, false/empty for normal behavior +# +# Returns: +# 0 on success, 1 on failure + +smart_apt_update() { + local force=${1:-false} + local timestamp_file="/tmp/deployer-apt-last-update" + local threshold_seconds=$((24 * 60 * 60)) # 24 hours + local now current_timestamp age + + now=$(date +%s) + + # Check if we need to update + if [[ $force == false && -f $timestamp_file ]]; then + current_timestamp=$(cat "$timestamp_file" 2> /dev/null || echo "0") + age=$((now - current_timestamp)) + + if ((age < threshold_seconds)); then + echo "→ Using cached package list (cached $((age / 3600)) hours ago)..." + return 0 + fi + fi + + # Perform update + echo "→ Updating package lists..." + if ! apt_get_with_retry update; then + echo "Error: Failed to update package lists" >&2 + return 1 + fi + + # Update timestamp + echo "$now" > "$timestamp_file" +} + +# +# Get PHP versions and extensions with caching +# +# Arguments: +# $1 - force (optional): true to bypass cache and re-detect +# +# Side effects: +# Sets PHP_CACHE_YAML with the YAML structure (or empty string) + +get_php_with_cache() { + PHP_CACHE_YAML="" + local force=${1:-false} + local cache_file="/tmp/deployer-php-cache" + local threshold_seconds=$((24 * 60 * 60)) # 24 hours + local now current_timestamp age + + # Check if PHP gathering is requested + if [[ $DEPLOYER_GATHER_PHP != 'true' ]]; then + return 0 + fi + + now=$(date +%s) + + # Check if we can use cache + if [[ $force == false && -f $cache_file ]]; then + # Read timestamp from first line of cache + current_timestamp=$(head -n1 "$cache_file" 2> /dev/null || echo "0") + age=$((now - current_timestamp)) + + if ((age < threshold_seconds)); then + echo "→ Using cached PHP version and extensions list (cached $((age / 3600)) hours ago)..." + # Return cached YAML (skip first line which is timestamp) + PHP_CACHE_YAML=$(tail -n +2 "$cache_file" 2> /dev/null || printf '') + return 0 + fi + fi + + # Perform fresh detection + echo "→ Detecting available PHP versions..." + + local php_versions + php_versions=$(apt-cache search "^php[0-9]+\.[0-9]+-fpm$" 2> /dev/null | grep -oP 'php\K[0-9]+\.[0-9]+' | sort -V -u) + + if [[ -z $php_versions ]]; then + echo "Error: No PHP versions found in repositories" >&2 + exit 1 + fi + + # Build YAML structure + local yaml_output="php:" + + echo "→ Detecting available PHP extensions..." + for version in $php_versions; do + local extensions + extensions=$(apt-cache search "^php${version}-" 2> /dev/null | grep -oP "php${version}-\K[a-z0-9]+" | sort -u) + + if [[ -z $extensions ]]; then + continue + fi + + yaml_output="${yaml_output}\n \"${version}\":" + yaml_output="${yaml_output}\n extensions:" + + for ext in $extensions; do + yaml_output="${yaml_output}\n - ${ext}" + done + done + + # Cache the results (timestamp on first line, YAML on subsequent lines) + { + echo "$now" + echo -e "$yaml_output" + } > "$cache_file" + + # Store the YAML for callers + PHP_CACHE_YAML="$yaml_output" +} + +# ---- +# Main Execution +# ---- + +main() { + local repo_added=false + + # + # Initial apt update + # ---- + + if ! smart_apt_update; then + exit 1 + fi + + # + # Caddy repository (same for both Ubuntu and Debian) + # ---- + + if ! [[ -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg ]]; then + echo "→ Adding Caddy GPG key..." + if ! curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | run_cmd gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg; then + echo "Error: Failed to add Caddy GPG key" >&2 + exit 1 + fi + repo_added=true + fi + + if ! [[ -f /etc/apt/sources.list.d/caddy-stable.list ]]; then + echo "→ Adding Caddy repository..." + if ! curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | run_cmd tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null; then + echo "Error: Failed to add Caddy repository" >&2 + exit 1 + fi + repo_added=true + fi + + # + # PHP repository + # ---- + + case $DEPLOYER_DISTRO in + ubuntu) + + # + # PHP PPA (Ubuntu only) + + if ! grep -qr "ondrej/php" /etc/apt/sources.list /etc/apt/sources.list.d/ 2> /dev/null; then + echo "→ Adding PHP PPA..." + if ! run_cmd env DEBIAN_FRONTEND=noninteractive add-apt-repository -y ppa:ondrej/php 2>&1; then + echo "Error: Failed to add PHP PPA" >&2 + exit 1 + fi + repo_added=true + fi + ;; + debian) + + # + # Sury PHP repository (Debian only) + + if ! [[ -f /usr/share/keyrings/php-sury-archive-keyring.gpg ]]; then + echo "→ Adding PHP Sury GPG key..." + if ! curl -fsSL 'https://packages.sury.org/php/apt.gpg' | run_cmd gpg --batch --yes --dearmor -o /usr/share/keyrings/php-sury-archive-keyring.gpg; then + echo "Error: Failed to add Sury PHP GPG key" >&2 + exit 1 + fi + repo_added=true + fi + + if ! [[ -f /etc/apt/sources.list.d/php-sury.list ]]; then + echo "→ Adding PHP Sury repository..." + local debian_codename + debian_codename=$(lsb_release -sc) + if ! echo "deb [signed-by=/usr/share/keyrings/php-sury-archive-keyring.gpg] https://packages.sury.org/php/ ${debian_codename} main" | run_cmd tee /etc/apt/sources.list.d/php-sury.list > /dev/null; then + echo "Error: Failed to add Sury PHP repository" >&2 + exit 1 + fi + repo_added=true + fi + ;; + esac + + # + # Update apt again, only if we added new repositories + # ---- + + if [[ $repo_added == true ]]; then + if ! smart_apt_update true; then + exit 1 + fi + fi + + # + # Detect PHP versions and extensions (optional, with caching) + # ---- + + local yaml_php="" + + # Pass force=true if repos were added to invalidate cache + if [[ $repo_added == true ]]; then + get_php_with_cache true + else + get_php_with_cache + fi + + yaml_php="$PHP_CACHE_YAML" + + # + # Write output YAML + # ---- + + if [[ -n $yaml_php ]]; then + { + echo "status: success" + echo "repos_configured: true" + printf '%b\n' "$yaml_php" + } > "$DEPLOYER_OUTPUT_FILE" + else + { + echo "status: success" + echo "repos_configured: true" + } > "$DEPLOYER_OUTPUT_FILE" + fi + + if [[ ! -f $DEPLOYER_OUTPUT_FILE ]]; then + echo "Error: Failed to write output file" >&2 + exit 1 + fi +} + +main "$@" diff --git a/playbooks/server-info.sh b/playbooks/server-info.sh index 9722dc68..1d5b6943 100755 --- a/playbooks/server-info.sh +++ b/playbooks/server-info.sh @@ -12,7 +12,7 @@ # - family: debian|fedora|redhat|amazon|unknown # - permissions: root|sudo|none # - hardware: cpu_cores, ram_mb, disk_type -# - php: versions array, default version +# - php: versions array (version, extensions), default version # - caddy: Caddy metrics (available, version, sites_count, domains, uptime_seconds, active_requests, total_requests, memory_mb) # - php_fpm: map of PHP versions to metrics (pool, process_manager, uptime_seconds, accepted_conn, listen_queue, idle_processes, active_processes, total_processes, max_children_reached, slow_requests) # - ports: map of port numbers to process names @@ -123,32 +123,6 @@ check_permissions() { # Helper Functions # ---- -# -# Tool Installation -# ---- - -# -# Ensure required networking tools are installed - -ensure_tools() { - local family=$1 perms=$2 - export DEPLOYER_PERMS=$perms - - # Check if required tools exist - command -v ss > /dev/null 2>&1 && command -v lsblk > /dev/null 2>&1 && return 0 - - case $family in - debian) - run_cmd apt-get update -q 2> /dev/null - run_cmd apt-get install -y -q iproute2 util-linux 2> /dev/null - ;; - fedora | redhat | amazon) - run_cmd yum install -y -q iproute util-linux 2> /dev/null \ - || run_cmd dnf install -y -q iproute util-linux 2> /dev/null - ;; - esac -} - # # Hardware Detection # ---- @@ -219,6 +193,29 @@ detect_php_versions() { fi } +# +# Detect PHP extensions for a specific version + +detect_php_extensions() { + local php_version=$1 + local extensions=() + + # Get extensions using php -m for the specific version + if command -v "php${php_version}" > /dev/null 2>&1; then + while IFS= read -r ext; do + [[ -n $ext ]] && extensions+=("$ext") + done < <("php${php_version}" -m 2> /dev/null | grep -v '^\[' | grep -v '^$') + fi + + # Return comma-separated list + if ((${#extensions[@]} > 0)); then + printf '%s' "$( + IFS=, + echo "${extensions[*]}" + )" + fi +} + # ---- # Service Metrics # ---- @@ -227,9 +224,6 @@ detect_php_versions() { # Listening Services # ---- -# -# Get all listening services and ports - get_listening_services() { local port process @@ -270,9 +264,6 @@ get_listening_services() { # Caddy Metrics # ---- -# -# Query Caddy admin API and extract metrics - get_caddy_metrics() { # Check if Caddy admin API is available on port 2019 if ! curl -sf --max-time 2 http://localhost:2019/config/ > /dev/null 2>&1; then @@ -334,9 +325,6 @@ get_caddy_metrics() { # PHP-FPM Metrics # ---- -# -# Query PHP-FPM status page for a specific PHP version - get_php_fpm_metrics() { local php_version=$1 @@ -400,26 +388,23 @@ main() { # # Gather basic info - echo "✓ Detecting distribution..." + echo "→ Detecting distribution..." distro=$(detect_distro) family=$(detect_family "$distro") - echo "✓ Checking permissions..." + echo "→ Checking permissions..." permissions=$(check_permissions) - echo "✓ Cataloging services..." - ensure_tools "$family" "$permissions" - - echo "✓ Detecting hardware..." + echo "→ Detecting hardware..." cpu_cores=$(detect_cpu_cores) ram_mb=$(detect_ram_mb) disk_type=$(detect_disk_type) - echo "✓ Detecting PHP versions..." + echo "→ Detecting PHP versions..." php_versions=$(detect_php_versions) php_default=$(detect_php_default) - echo "✓ Checking Caddy status..." + echo "→ Checking Caddy status..." local caddy_metrics caddy_available="false" local caddy_version caddy_sites caddy_domains caddy_uptime caddy_active_req caddy_total_req caddy_memory caddy_metrics=$(get_caddy_metrics) @@ -429,7 +414,34 @@ main() { IFS=$'\t' read -r caddy_version caddy_sites caddy_domains caddy_uptime caddy_active_req caddy_total_req caddy_memory <<< "$caddy_metrics" fi - echo "✓ Checking PHP-FPM status..." + echo "→ Checking PHP-FPM status..." + local php_fpm_yaml="" has_fpm_metrics=false + if [[ -n $php_versions ]]; then + IFS=',' read -ra version_array <<< "$php_versions" + for version in "${version_array[@]}"; do + local fpm_metrics + fpm_metrics=$(get_php_fpm_metrics "$version") + + if [[ -n $fpm_metrics ]]; then + has_fpm_metrics=true + local pool pm uptime accepted queue idle active total max_children slow + IFS=$'\t' read -r pool pm uptime accepted queue idle active total max_children slow <<< "$fpm_metrics" + + php_fpm_yaml+=" \"${version}\": + pool: ${pool} + process_manager: ${pm} + uptime_seconds: ${uptime} + accepted_conn: ${accepted} + listen_queue: ${queue} + idle_processes: ${idle} + active_processes: ${active} + total_processes: ${total} + max_children_reached: ${max_children} + slow_requests: ${slow} +" + fi + done + fi # # Output YAML to file @@ -443,8 +455,38 @@ main() { ram_mb: $ram_mb disk_type: $disk_type php: - versions: [${php_versions}] default: ${php_default:-} + versions: + EOF + echo "Error: Failed to write output file header to $DEPLOYER_OUTPUT_FILE" >&2 + exit 1 + fi + + # Add PHP versions with extensions + if [[ -n $php_versions ]]; then + IFS=',' read -ra version_array <<< "$php_versions" + for version in "${version_array[@]}"; do + local extensions + extensions=$(detect_php_extensions "$version") + + if ! cat >> "$DEPLOYER_OUTPUT_FILE" <<- EOF; then + - version: "${version}" + extensions: [${extensions}] + EOF + echo "Error: Failed to write PHP version $version to $DEPLOYER_OUTPUT_FILE" >&2 + exit 1 + fi + done + else + # No PHP versions found, write empty array + if ! echo " []" >> "$DEPLOYER_OUTPUT_FILE"; then + echo "Error: Failed to write empty PHP versions to $DEPLOYER_OUTPUT_FILE" >&2 + exit 1 + fi + fi + + # Continue with Caddy and PHP-FPM sections + if ! cat >> "$DEPLOYER_OUTPUT_FILE" <<- EOF; then caddy: available: $caddy_available version: ${caddy_version:-unknown} @@ -456,45 +498,17 @@ main() { memory_mb: ${caddy_memory:-0} php_fpm: EOF - echo "Error: Failed to write $DEPLOYER_OUTPUT_FILE" >&2 + echo "Error: Failed to write Caddy metrics to $DEPLOYER_OUTPUT_FILE" >&2 exit 1 fi - # Add PHP-FPM metrics for each installed version - local has_fpm_metrics=false - if [[ -n $php_versions ]]; then - IFS=',' read -ra version_array <<< "$php_versions" - for version in "${version_array[@]}"; do - local fpm_metrics - fpm_metrics=$(get_php_fpm_metrics "$version") - - if [[ -n $fpm_metrics ]]; then - has_fpm_metrics=true - local pool pm uptime accepted queue idle active total max_children slow - IFS=$'\t' read -r pool pm uptime accepted queue idle active total max_children slow <<< "$fpm_metrics" - - if ! cat >> "$DEPLOYER_OUTPUT_FILE" <<- EOF; then - "${version}": - pool: ${pool} - process_manager: ${pm} - uptime_seconds: ${uptime} - accepted_conn: ${accepted} - listen_queue: ${queue} - idle_processes: ${idle} - active_processes: ${active} - total_processes: ${total} - max_children_reached: ${max_children} - slow_requests: ${slow} - EOF - echo "Error: Failed to write PHP-FPM metrics to $DEPLOYER_OUTPUT_FILE" >&2 - exit 1 - fi - fi - done - fi - - # If no PHP-FPM metrics were found, write empty object - if [[ $has_fpm_metrics == false ]]; then + # Write PHP-FPM metrics (gathered earlier) + if [[ $has_fpm_metrics == true ]]; then + if ! echo "$php_fpm_yaml" >> "$DEPLOYER_OUTPUT_FILE"; then + echo "Error: Failed to write PHP-FPM metrics to $DEPLOYER_OUTPUT_FILE" >&2 + exit 1 + fi + else if ! echo " {}" >> "$DEPLOYER_OUTPUT_FILE"; then echo "Error: Failed to write empty PHP-FPM section to $DEPLOYER_OUTPUT_FILE" >&2 exit 1 @@ -503,14 +517,14 @@ main() { # Add ports section if ! echo "ports:" >> "$DEPLOYER_OUTPUT_FILE"; then - echo "Error: Failed to write ports section to $DEPLOYER_OUTPUT_FILE" >&2 + echo "Error: Failed to write ports section header to $DEPLOYER_OUTPUT_FILE" >&2 exit 1 fi local port process has_ports=false while IFS=: read -r port process; do if ! echo " ${port}: ${process}" >> "$DEPLOYER_OUTPUT_FILE"; then - echo "Error: Failed to write services list to $DEPLOYER_OUTPUT_FILE" >&2 + echo "Error: Failed to write port $port to $DEPLOYER_OUTPUT_FILE" >&2 exit 1 fi has_ports=true @@ -518,7 +532,7 @@ main() { if [[ $has_ports == false ]]; then if ! echo " {}" >> "$DEPLOYER_OUTPUT_FILE"; then - echo "Error: Failed to write empty services lists to $DEPLOYER_OUTPUT_FILE" >&2 + echo "Error: Failed to write empty ports section to $DEPLOYER_OUTPUT_FILE" >&2 exit 1 fi fi diff --git a/playbooks/server-install.sh b/playbooks/server-install.sh deleted file mode 100644 index 2f49e83b..00000000 --- a/playbooks/server-install.sh +++ /dev/null @@ -1,429 +0,0 @@ -#!/usr/bin/env bash - -# -# Server Installation Playbook - Ubuntu/Debian Only -# -# Install Caddy, Git, Bun, and setup deploy user -# ---- -# -# This playbook only supports Ubuntu and Debian distributions (debian family). -# Both distributions use apt package manager and follow debian conventions. -# -# Note: PHP installation is handled by a separate playbook (server-install-php.sh) -# -# Required Environment Variables: -# DEPLOYER_OUTPUT_FILE - Output file path -# DEPLOYER_DISTRO - Exact distribution: ubuntu|debian -# DEPLOYER_PERMS - Permissions: root|sudo -# DEPLOYER_SERVER_NAME - Server name for deploy key generation -# -# Returns YAML with: -# - status: success -# - distro: detected distribution -# - caddy_version: installed Caddy version -# - git_version: installed Git version -# - bun_version: installed Bun version -# - deploy_public_key: public key for git deployments -# - tasks_completed: list of completed tasks -# - -set -o pipefail -export DEBIAN_FRONTEND=noninteractive - -[[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 -[[ -z $DEPLOYER_DISTRO ]] && echo "Error: DEPLOYER_DISTRO required" && exit 1 -[[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 -[[ -z $DEPLOYER_SERVER_NAME ]] && echo "Error: DEPLOYER_SERVER_NAME required" && exit 1 -export DEPLOYER_PERMS - -# Shared helpers are automatically inlined when executing playbooks remotely -# source "$(dirname "$0")/helpers.sh" - -# ---- -# Installation Functions -# ---- - -# -# Repository Setup -# ---- - -# -# Setup distribution-specific repositories - -setup_repositories() { - echo "✓ Setting up repositories..." - - # Caddy repository (same for both Ubuntu and Debian) - if ! [[ -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg ]]; then - if ! curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | run_cmd gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg; then - echo "Error: Failed to add Caddy GPG key" >&2 - exit 1 - fi - fi - - if ! [[ -f /etc/apt/sources.list.d/caddy-stable.list ]]; then - if ! curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | run_cmd tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null; then - echo "Error: Failed to add Caddy repository" >&2 - exit 1 - fi - fi -} - -# -# Package Installation -# ---- - -# -# Install all required packages (Caddy, PHP, Git, system utilities) - -install_all_packages() { - echo "✓ Installing all packages..." - - # Update package lists - echo "✓ Updating package lists..." - if ! apt_get_with_retry update -q; then - echo "Error: Failed to update package lists" >&2 - exit 1 - fi - - # Install prerequisites based on distribution - echo "✓ Installing prerequisites..." - case $DEPLOYER_DISTRO in - ubuntu) - if ! apt_get_with_retry install -y -q curl software-properties-common; then - echo "Error: Failed to install prerequisites" >&2 - exit 1 - fi - ;; - debian) - if ! apt_get_with_retry install -y -q curl apt-transport-https lsb-release ca-certificates; then - echo "Error: Failed to install prerequisites" >&2 - exit 1 - fi - ;; - esac - - # Setup repositories (requires prerequisites) - setup_repositories - - # Update package lists again (after adding repositories) - echo "✓ Updating package lists..." - if ! apt_get_with_retry update -q; then - echo "Error: Failed to update package lists" >&2 - exit 1 - fi - - # Install system utilities - echo "✓ Installing system utilities..." - if ! apt_get_with_retry install -y -q unzip; then - echo "Error: Failed to install system utilities" >&2 - exit 1 - fi - - # Install main packages - echo "✓ Installing main packages..." - if ! apt_get_with_retry install -y -q caddy git rsync; then - echo "Error: Failed to install main packages" >&2 - exit 1 - fi -} - -# -# Install Bun runtime - -install_bun() { - if command -v bun > /dev/null 2>&1; then - echo "✓ Bun already installed" - return 0 - fi - - echo "✓ Installing Bun..." - - # Install Bun system-wide to /usr/local (unzip is now installed in batched packages) - if ! curl -fsSL https://bun.sh/install | run_cmd env BUN_INSTALL=/usr/local bash; then - echo "Error: Failed to install Bun" >&2 - exit 1 - fi -} - -# -# Caddy Configuration -# ---- - -# -# Setup Caddy configuration structure - -setup_caddy_structure() { - echo "✓ Setting up Caddy configuration structure..." - - # Create directory structure - if ! run_cmd mkdir -p /etc/caddy/conf.d/sites; then - echo "Error: Failed to create Caddy config directories" >&2 - exit 1 - fi - - # Create main Caddyfile with global settings and imports - if ! run_cmd tee /etc/caddy/Caddyfile > /dev/null <<- 'EOF'; then - { - metrics - - log { - output file /var/log/caddy/access.log - format json - } - } - - # Import localhost-only endpoints (monitoring, status pages) - import conf.d/localhost.caddy - - # Import all site configurations - import conf.d/sites/*.caddy - EOF - echo "Error: Failed to create main Caddyfile" >&2 - exit 1 - fi - - # Create localhost.caddy - monitoring endpoints only accessible via localhost - # (PHP-FPM status endpoint will be added by PHP installation playbook) - if ! run_cmd tee /etc/caddy/conf.d/localhost.caddy > /dev/null <<- 'EOF'; then - # PHP-FPM status endpoints - localhost only (not accessible from internet) - http://localhost:9001 { - #### DEPLOYER-PHP CONFIG, WARRANTY VOID IF REMOVED :) #### - } - EOF - echo "Error: Failed to create localhost.caddy" >&2 - exit 1 - fi -} - -# -# Deployer User Setup -# ---- - -# -# Ensure deployer user exists - -ensure_deployer_user() { - if id -u deployer > /dev/null 2>&1; then - echo "✓ Deployer user already exists" - return 0 - fi - - echo "✓ Creating deployer user..." - if ! run_cmd useradd -m -s /bin/bash deployer; then - echo "Error: Failed to create deployer user" >&2 - exit 1 - fi -} - -# -# Configure group memberships for file access - -configure_deployer_groups() { - # Add caddy user to deployer group so it can access deployer's files - if ! id -nG caddy 2> /dev/null | grep -qw deployer; then - echo "✓ Adding caddy user to deployer group..." - if ! run_cmd usermod -aG deployer caddy; then - echo "Error: Failed to add caddy to deployer group" >&2 - exit 1 - fi - - # Restart Caddy so it picks up the new group membership - if systemctl is-active --quiet caddy 2> /dev/null; then - echo "✓ Restarting Caddy to apply group membership..." - if ! run_cmd systemctl restart caddy; then - echo "Error: Failed to restart Caddy" >&2 - exit 1 - fi - fi - fi - -} - -# -# Setup deploy user with proper home directory and permissions - -setup_deploy_user() { - ensure_deployer_user - - local deployer_home - deployer_home=$(getent passwd deployer | cut -d: -f6) - - if [[ -z $deployer_home ]]; then - echo "Error: Unable to determine deployer home directory" >&2 - exit 1 - fi - - if ! run_cmd test -d "$deployer_home"; then - if ! run_cmd mkdir -p "$deployer_home"; then - echo "Error: Failed to create deployer home directory" >&2 - exit 1 - fi - fi - - if ! run_cmd chown deployer:deployer "$deployer_home"; then - echo "Error: Failed to set ownership on deployer home directory" >&2 - exit 1 - fi - - if ! run_cmd chmod 750 "$deployer_home"; then - echo "Error: Failed to set permissions on deployer home directory" >&2 - exit 1 - fi - - configure_deployer_groups -} - -# -# Deploy Key Setup -# ---- - -# -# Generate SSH deploy key for git operations - -setup_deploy_key() { - echo "✓ Setting up deploy key..." - - setup_deploy_user - - local deployer_home - deployer_home=$(getent passwd deployer | cut -d: -f6) - local deployer_ssh_dir - deployer_ssh_dir="${deployer_home}/.ssh" - local private_key - private_key="${deployer_ssh_dir}/id_ed25519" - local public_key - public_key="${deployer_ssh_dir}/id_ed25519.pub" - - # Create .ssh directory if it doesn't exist - if ! run_cmd test -d "$deployer_ssh_dir"; then - if ! run_cmd mkdir -p "$deployer_ssh_dir"; then - echo "Error: Failed to create .ssh directory" >&2 - exit 1 - fi - fi - - # Generate key pair if it doesn't exist - if ! run_cmd test -f "$private_key"; then - echo "✓ Generating SSH key pair..." - if ! run_cmd ssh-keygen -t ed25519 -C "deployer@${DEPLOYER_SERVER_NAME}" -f "$private_key" -N ""; then - echo "Error: Failed to generate SSH key pair" >&2 - exit 1 - fi - else - echo "✓ SSH key pair already exists" - fi - - # Set proper ownership and permissions - if ! run_cmd chown -R deployer:deployer "$deployer_ssh_dir"; then - echo "Error: Failed to set ownership on .ssh directory" >&2 - exit 1 - fi - - if ! run_cmd chmod 700 "$deployer_ssh_dir"; then - echo "Error: Failed to set permissions on .ssh directory" >&2 - exit 1 - fi - - if ! run_cmd chmod 600 "$private_key"; then - echo "Error: Failed to set permissions on private key" >&2 - exit 1 - fi - - if ! run_cmd chmod 644 "$public_key"; then - echo "Error: Failed to set permissions on public key" >&2 - exit 1 - fi -} - -# -# Ensure proper permissions on deploy directories - -setup_deploy_directories() { - if ! run_cmd test -d /home/deployer; then - echo "Error: Deployer home directory missing" >&2 - exit 1 - fi - - # Ensure home directory permissions - if ! run_cmd chmod 750 /home/deployer; then - echo "Error: Failed to set permissions on deployer home" >&2 - exit 1 - fi - - # Ensure demo directory structure ownership if present - if run_cmd test -d /home/deployer/demo; then - if ! run_cmd chown -R deployer:deployer /home/deployer/demo; then - echo "Error: Failed to set ownership on demo directory" >&2 - exit 1 - fi - - if ! run_cmd chmod 750 /home/deployer/demo; then - echo "Error: Failed to set permissions on demo directory" >&2 - exit 1 - fi - - if run_cmd test -d /home/deployer/demo/public; then - if ! run_cmd chmod 750 /home/deployer/demo/public; then - echo "Error: Failed to set permissions on public directory" >&2 - exit 1 - fi - - if run_cmd test -f /home/deployer/demo/public/index.php; then - if ! run_cmd chmod 640 /home/deployer/demo/public/index.php; then - echo "Error: Failed to set permissions on index.php" >&2 - exit 1 - fi - fi - fi - fi -} - -# -# Validation -# ---- - -# ---- -# Main Execution -# ---- - -main() { - local caddy_version bun_version git_version deploy_public_key - - # Execute installation tasks - install_all_packages - install_bun - setup_caddy_structure - setup_deploy_key - setup_deploy_directories - - # Get versions and public key - caddy_version=$(caddy version 2> /dev/null | head -n1 | awk '{print $1}' || echo "unknown") - git_version=$(git --version 2> /dev/null | awk '{print $3}' || echo "unknown") - bun_version=$(bun --version 2> /dev/null || echo "unknown") - deploy_public_key=$(run_cmd cat /home/deployer/.ssh/id_ed25519.pub 2> /dev/null || echo "unknown") - - # Write output YAML - if ! cat > "$DEPLOYER_OUTPUT_FILE" <<- EOF; then - status: success - distro: $DEPLOYER_DISTRO - caddy_version: $caddy_version - git_version: $git_version - bun_version: $bun_version - deploy_public_key: $deploy_public_key - tasks_completed: - - install_caddy - - setup_caddy_structure - - install_git - - install_rsync - - install_bun - - setup_deploy_user - - setup_deploy_key - - setup_deploy_directories - EOF - echo "Error: Failed to write output file" >&2 - exit 1 - fi -} - -main "$@"