diff --git a/app/Console/Server/ServerAddCommand.php b/app/Console/Server/ServerAddCommand.php index 40cd8fd3..3de7e308 100644 --- a/app/Console/Server/ServerAddCommand.php +++ b/app/Console/Server/ServerAddCommand.php @@ -84,7 +84,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->displayServerDeets($server); // - // Get server info (verifies SSH connection and validates distribution) + // Get server info (verifies SSH connection and validates distribution & permissions) // ---- $info = $this->getServerInfo($server); diff --git a/app/Console/Server/ServerInfoCommand.php b/app/Console/Server/ServerInfoCommand.php index a5fc119e..8c60000c 100644 --- a/app/Console/Server/ServerInfoCommand.php +++ b/app/Console/Server/ServerInfoCommand.php @@ -56,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->displayServerDeets($server); // - // Get server info (verifies SSH connection and validates distribution) + // Get server info (verifies SSH connection and validates distribution & permissions) // ---- $info = $this->getServerInfo($server); diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index abd9130f..bf35c606 100644 --- a/app/Console/Server/ServerInstallCommand.php +++ b/app/Console/Server/ServerInstallCommand.php @@ -24,7 +24,8 @@ class ServerInstallCommand extends BaseCommand use PlaybooksTrait; use ServersTrait; - // ---- Configuration + // ---- + // Configuration // ---- protected function configure(): void @@ -34,7 +35,7 @@ protected function configure(): void $this->addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name'); } - // + // ---- // Execution // ---- @@ -57,7 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->displayServerDeets($server); // - // Get server info (verifies SSH connection and validates distribution) + // Get server info (verifies SSH connection and validates distribution & permissions) // ---- $info = $this->getServerInfo($server); @@ -66,31 +67,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $info; } - // - // Validate server info - // ---- + [ + 'distro' => $distro, + 'permissions' => $permissions, + ] = $info; /** @var string $distro */ - $distro = $info['distro'] ?? 'unknown'; - $distribution = Distribution::tryFrom($distro); - if ($distribution === null) { - $this->nay("Distribution validation failed: {$distro}"); - - return Command::FAILURE; - } - - $permissions = $info['permissions'] ?? null; - if (!is_string($permissions) || !in_array($permissions, ['root', 'sudo'])) { - $this->nay('Server requires root or sudo permissions to install software'); - - return Command::FAILURE; - } - - $family = $distribution->family()->value; + /** @var string $permissions */ // // Execute installation playbook - // --- + // ---- $result = $this->executePlaybook( $server, @@ -98,8 +85,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'Installing server...', [ 'DEPLOYER_DISTRO' => $distro, - 'DEPLOYER_FAMILY' => $family, 'DEPLOYER_PERMS' => $permissions, + 'DEPLOYER_SERVER_NAME' => $server->name, ], true ); @@ -116,12 +103,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Setup demo site // ---- + /** @var string $permissions */ $demoResult = $this->executePlaybook( $server, 'demo-site', 'Setting up demo site...', [ - 'DEPLOYER_FAMILY' => $family, 'DEPLOYER_PERMS' => $permissions, ], true @@ -140,8 +127,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int // ---- $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; + $verification = $this->io->promptSpin( - fn () => $this->verifyInstallation($url), + fn () => $this->verifyInstallation($url, $deployKey), 'Verifying installation...' ); @@ -175,7 +166,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int * * @return array{status: 'success'|'warning', message: string, lines: array} */ - private function verifyInstallation(string $url): array + private function verifyInstallation(string $url, ?string $deployKey): array { try { $client = new Client([ @@ -203,15 +194,24 @@ private function verifyInstallation(string $url): array ]; } + $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' => [ - 'Next steps:', - ' • Caddy running at ' . $url . '', - ' • Run site:add to deploy your first application', - '', - ], + 'lines' => $nextSteps, ]; } catch (\Throwable $e) { return [ diff --git a/app/Console/Server/ServerLogsCommand.php b/app/Console/Server/ServerLogsCommand.php index 0d7a49e8..26e43032 100644 --- a/app/Console/Server/ServerLogsCommand.php +++ b/app/Console/Server/ServerLogsCommand.php @@ -62,7 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->displayServerDeets($server); // - // Get server info (verifies SSH connection and validates distribution) + // Get server info (verifies SSH connection and validates distribution & permissions) // ---- $info = $this->getServerInfo($server); diff --git a/app/Console/Server/ServerProvisionDigitalOceanCommand.php b/app/Console/Server/ServerProvisionDigitalOceanCommand.php index e5cc80bf..5b02425c 100644 --- a/app/Console/Server/ServerProvisionDigitalOceanCommand.php +++ b/app/Console/Server/ServerProvisionDigitalOceanCommand.php @@ -164,7 +164,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->displayServerDeets($server); - // Get server info (verifies SSH connection and validates distribution) + // Get server info (verifies SSH connection and validates distribution & permissions) $info = $this->getServerInfo($server); if (is_int($info)) { diff --git a/app/Traits/PlaybooksTrait.php b/app/Traits/PlaybooksTrait.php index 3695ae25..ad20b163 100644 --- a/app/Traits/PlaybooksTrait.php +++ b/app/Traits/PlaybooksTrait.php @@ -34,8 +34,7 @@ trait PlaybooksTrait * * Standard playbook environment variables: * - DEPLOYER_OUTPUT_FILE: Output file path (provided automatically) - * - DEPLOYER_DISTRO: Exact distribution - caller must provide via $playbookVars - * - DEPLOYER_FAMILY: Distribution family - caller must provide via $playbookVars + * - DEPLOYER_DISTRO: Exact distribution (ubuntu|debian) - caller must provide via $playbookVars * - DEPLOYER_PERMS: User permissions (root|sudo|none) - caller must provide via $playbookVars * * @param string $playbookName Playbook name without .sh extension (e.g., 'server-info', 'install-php', etc) diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index 2ec40c28..8129cfa4 100644 --- a/app/Traits/ServersTrait.php +++ b/app/Traits/ServersTrait.php @@ -38,7 +38,8 @@ trait ServersTrait /** * Get server information by executing server-info playbook. * - * Automatically displays server info and validates that the server is running a supported distribution (Debian/Ubuntu). + * Automatically displays server info and validates that the server is running a supported distribution (Debian/Ubuntu) + * and has sufficient permissions (root or sudo). * * @param ServerDTO $server Server to get information for * @return array|int Returns parsed server info or failure code on failure @@ -58,7 +59,15 @@ protected function getServerInfo(ServerDTO $server): array|int // Display server information before validation $this->displayServerInfo($info); - return $this->validateServerDistribution($info); + // Validate server distribution and permissions + $distroResult = $this->validateServerDistribution($info); + $permissionsResult = $this->validateServerPermissions($info); + + if (is_int($distroResult) || is_int($permissionsResult)) { + return Command::FAILURE; + } + + return $info; } /** @@ -90,6 +99,25 @@ protected function validateServerDistribution(array $info): array|int return $info; } + /** + * Validate that server has sufficient permissions (root or sudo). + * + * @param array $info Server information array from server-info playbook + * @return array|int Returns validated server info or failure code + */ + protected function validateServerPermissions(array $info): array|int + { + $permissions = $info['permissions'] ?? null; + + if (!is_string($permissions) || !in_array($permissions, ['root', 'sudo'])) { + $this->nay('Server requires root or sudo permissions'); + + return Command::FAILURE; + } + + return $info; + } + /** * Display formatted server information. * @@ -319,6 +347,11 @@ protected function validateServerName(mixed $name): ?string return 'Server name cannot be empty'; } + // Validate format: alphanumeric, hyphens, underscores only + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { + return 'Server name can only contain letters, numbers, hyphens, and underscores'; + } + // Check uniqueness $existing = $this->servers->findByName($name); if ($existing !== null) { diff --git a/playbooks/demo-site.sh b/playbooks/demo-site.sh index b8caea52..e6802659 100644 --- a/playbooks/demo-site.sh +++ b/playbooks/demo-site.sh @@ -1,20 +1,21 @@ #!/usr/bin/env bash # -# Demo Site Setup Playbook +# Demo Site Setup Playbook - Ubuntu/Debian Only # -# Create deployer user, configure permissions, setup demo site +# Provision demo site (requires deployer user pre-configured) # ---- # +# This playbook only supports Ubuntu and Debian distributions (debian family). +# # Required Environment Variables: # DEPLOYER_OUTPUT_FILE - Output file path -# DEPLOYER_FAMILY - Distribution family: debian|fedora|redhat|amazon # DEPLOYER_PERMS - Permissions: root|sudo # # Returns YAML with: # - status: success # - demo_site_path: /home/deployer/demo/public -# - deployer_user: created +# - deployer_user: existing # - caddy_configured: true # @@ -22,7 +23,6 @@ set -o pipefail export DEBIAN_FRONTEND=noninteractive [[ -z $DEPLOYER_OUTPUT_FILE ]] && echo "Error: DEPLOYER_OUTPUT_FILE required" && exit 1 -[[ -z $DEPLOYER_FAMILY ]] && echo "Error: DEPLOYER_FAMILY required" && exit 1 [[ -z $DEPLOYER_PERMS ]] && echo "Error: DEPLOYER_PERMS required" && exit 1 export DEPLOYER_PERMS @@ -41,95 +41,18 @@ run_cmd() { fi } -# -# Get PHP-FPM user dynamically - -get_php_fpm_user() { - if [[ $DEPLOYER_FAMILY == 'debian' ]]; then - echo 'www-data' - else - local config_file='/etc/php-fpm.d/www.conf' - if [[ -f $config_file ]]; then - local user - user=$(grep -E '^\s*user\s*=' "$config_file" | awk '{print $3}' | tr -d ';') - if [[ -n $user ]]; then - echo "$user" - else - echo 'apache' - fi - else - echo 'apache' - fi - fi -} - -# -# Get PHP-FPM service name - -get_php_fpm_service() { - if [[ $DEPLOYER_FAMILY == 'debian' ]]; then - echo 'php8.4-fpm' - else - echo 'php-fpm' - fi -} - # # Setup Functions -create_deployer_user() { - if id -u deployer > /dev/null 2>&1; then - echo "✓ Deployer user already exists" - else - 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 - - # 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 +require_deployer_user() { + if ! id -u deployer > /dev/null 2>&1; then + echo "Error: Deployer user not found. Run server:install before demo-site." >&2 + exit 1 fi - # Add PHP-FPM user to deployer group so it can access files - local php_fpm_user php_fpm_service - php_fpm_user=$(get_php_fpm_user) - php_fpm_service=$(get_php_fpm_service) - - if id -u "$php_fpm_user" > /dev/null 2>&1; then - if ! id -nG "$php_fpm_user" 2> /dev/null | grep -qw deployer; then - echo "✓ Adding $php_fpm_user user to deployer group..." - if ! run_cmd usermod -aG deployer "$php_fpm_user"; then - echo "Error: Failed to add $php_fpm_user to deployer group" >&2 - exit 1 - fi - - # Restart PHP-FPM so it picks up the new group membership - if systemctl is-active --quiet "$php_fpm_service" 2> /dev/null; then - echo "✓ Restarting PHP-FPM to apply group membership..." - if ! run_cmd systemctl restart "$php_fpm_service"; then - echo "Error: Failed to restart PHP-FPM" >&2 - exit 1 - fi - fi - fi - else - echo "Warning: PHP-FPM user '$php_fpm_user' not found, skipping group assignment" + if ! run_cmd test -d /home/deployer; then + echo "Error: Deployer home directory missing. Run server:install before demo-site." >&2 + exit 1 fi } @@ -137,7 +60,7 @@ setup_demo_site() { echo "✓ Setting up demo site..." # Create directory structure - if [[ ! -d /home/deployer/demo/public ]]; then + if ! run_cmd test -d /home/deployer/demo/public; then if ! run_cmd mkdir -p /home/deployer/demo/public; then echo "Error: Failed to create demo site directory" >&2 exit 1 @@ -145,7 +68,7 @@ setup_demo_site() { fi # Create index.php - if [[ ! -f /home/deployer/demo/public/index.php ]]; then + if ! run_cmd test -f /home/deployer/demo/public/index.php; then if ! run_cmd tee /home/deployer/demo/public/index.php > /dev/null <<- 'EOF'; then "$DEPLOYER_OUTPUT_FILE" <<- EOF; then status: success demo_site_path: /home/deployer/demo/public - deployer_user: created + deployer_user: existing caddy_configured: true EOF echo "Error: Failed to write output file" >&2 diff --git a/playbooks/server-install.sh b/playbooks/server-install.sh index 91cdba1f..4c24195b 100644 --- a/playbooks/server-install.sh +++ b/playbooks/server-install.sh @@ -1,16 +1,19 @@ #!/usr/bin/env bash # -# Server Installation Playbook - Debian Family (Ubuntu, Debian) +# Server Installation Playbook - Ubuntu/Debian Only # # Install Caddy, PHP 8.4, PHP-FPM, Git, Bun # ---- # +# 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_FAMILY - Distribution family: debian # DEPLOYER_PERMS - Permissions: root|sudo +# DEPLOYER_SERVER_NAME - Server name for deploy key generation # # Returns YAML with: # - status: success @@ -19,6 +22,7 @@ # - 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 # @@ -27,8 +31,8 @@ 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_FAMILY ]] && echo "Error: DEPLOYER_FAMILY 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 # @@ -126,43 +130,30 @@ apt_get_with_retry() { # Installation Functions # ---- -install_all_packages() { - echo "✓ Installing all packages..." - - case $DEPLOYER_DISTRO in - ubuntu) - # Update package lists FIRST - 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 (now that package lists are updated) - echo "✓ Installing prerequisites..." - if ! apt_get_with_retry install -y -q curl software-properties-common; then - echo "Error: Failed to install prerequisites" >&2 - exit 1 - fi +# +# Setup distribution-specific repositories - # Add all repositories (now that prerequisites are installed) - echo "✓ Setting up repositories..." +setup_repositories() { + echo "✓ Setting up repositories..." - # Caddy repository (curl is now available) - 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 + # 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 + 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 + # PHP repository (distribution-specific) + 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 @@ -170,79 +161,9 @@ install_all_packages() { exit 1 fi fi - - # 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 remaining packages in batched groups - 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 - - 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 - - echo "✓ Installing PHP 8.4..." - if ! apt_get_with_retry install -y -q --no-install-recommends \ - php8.4-cli \ - php8.4-fpm \ - php8.4-common \ - php8.4-opcache \ - php8.4-bcmath \ - php8.4-curl \ - php8.4-mbstring \ - php8.4-xml \ - php8.4-zip \ - php8.4-gd \ - php8.4-intl \ - php8.4-soap 2>&1; then - echo "Error: Failed to install PHP 8.4 packages" >&2 - exit 1 - fi ;; debian) - # Update package lists FIRST - 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 (now that package lists are updated) - echo "✓ Installing prerequisites..." - 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 - - # Add all repositories (now that prerequisites are installed) - echo "✓ Setting up repositories..." - - # Caddy repository (curl is now available) - 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 - - # Sury PHP repository (Debian native - NOT a PPA) + # 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 @@ -258,47 +179,80 @@ install_all_packages() { exit 1 fi fi + ;; + esac +} - # 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_all_packages() { + echo "✓ Installing all packages..." - # Install remaining packages in batched groups - 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 + # 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 - echo "✓ Installing main packages..." - if ! apt_get_with_retry install -y -q caddy git rsync; then - echo "Error: Failed to install main packages" >&2 + # 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 - - echo "✓ Installing PHP 8.4..." - if ! apt_get_with_retry install -y -q --no-install-recommends \ - php8.4-cli \ - php8.4-fpm \ - php8.4-common \ - php8.4-opcache \ - php8.4-bcmath \ - php8.4-curl \ - php8.4-mbstring \ - php8.4-xml \ - php8.4-zip \ - php8.4-gd \ - php8.4-intl \ - php8.4-soap 2>&1; then - echo "Error: Failed to install PHP 8.4 packages" >&2 + ;; + 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 PHP 8.4 + echo "✓ Installing PHP 8.4..." + if ! apt_get_with_retry install -y -q --no-install-recommends \ + php8.4-cli \ + php8.4-fpm \ + php8.4-common \ + php8.4-opcache \ + php8.4-bcmath \ + php8.4-curl \ + php8.4-mbstring \ + php8.4-xml \ + php8.4-zip \ + php8.4-gd \ + php8.4-intl \ + php8.4-soap 2>&1; then + echo "Error: Failed to install PHP 8.4 packages" >&2 + exit 1 + fi + # Configure PHP-FPM echo "✓ Configuring PHP-FPM..." @@ -345,6 +299,187 @@ install_bun() { fi } +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_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 + + # 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 php8.4-fpm 2> /dev/null; then + echo "✓ Restarting PHP-FPM to apply group membership..." + if ! run_cmd systemctl restart php8.4-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" + fi +} + +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 +} + +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 +} + +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 +} + validate_php_version() { local php_version php_version=$(php -r "echo PHP_VERSION;" 2> /dev/null || echo "unknown") @@ -372,18 +507,21 @@ validate_php_version() { # ---- main() { - local php_version caddy_version bun_version git_version + local php_version caddy_version bun_version git_version deploy_public_key # Execute installation tasks install_all_packages install_bun validate_php_version + setup_deploy_key + setup_deploy_directories - # Get versions + # Get versions and public key php_version=$(php -r "echo PHP_VERSION;" 2> /dev/null || echo "unknown") 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 @@ -393,6 +531,7 @@ main() { caddy_version: $caddy_version git_version: $git_version bun_version: $bun_version + deploy_public_key: $deploy_public_key tasks_completed: - install_caddy - install_php @@ -401,6 +540,9 @@ main() { - 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