From 2a45cd847e176b63cd3500c32602d41c0098d024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sat, 8 Nov 2025 22:20:57 +0200 Subject: [PATCH 1/9] feat: add server name format validation - Validate server names contain only letters, numbers, hyphens, and underscores - Improve user experience with clear validation error messages --- app/Traits/ServersTrait.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index 2ec40c28..4bad86d5 100644 --- a/app/Traits/ServersTrait.php +++ b/app/Traits/ServersTrait.php @@ -319,6 +319,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) { From 4da2be434bb8086456689f39810881c2f448d5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sat, 8 Nov 2025 22:21:00 +0200 Subject: [PATCH 2/9] refactor: separate demo site setup from user creation - Require deployer user to already exist in demo-site.sh - Remove user creation logic from demo site playbook - Update output to reflect 'existing' deployer user status - Better separation of installation phases --- playbooks/demo-site.sh | 101 +++++------------------------------------ 1 file changed, 12 insertions(+), 89 deletions(-) diff --git a/playbooks/demo-site.sh b/playbooks/demo-site.sh index b8caea52..b6f49498 100644 --- a/playbooks/demo-site.sh +++ b/playbooks/demo-site.sh @@ -3,7 +3,7 @@ # # Demo Site Setup Playbook # -# Create deployer user, configure permissions, setup demo site +# Provision demo site (requires deployer user pre-configured) # ---- # # Required Environment Variables: @@ -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 From d76ffc8b8b0ead0840c46380f971668abcfb4068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sat, 8 Nov 2025 22:21:03 +0200 Subject: [PATCH 3/9] feat: add automatic deploy key generation and user setup - Generate SSH deploy keys automatically during server installation - Setup deployer user with proper permissions and group membership - Configure PHP-FPM and Caddy integration with deployer user - Return deploy public key for Git provider configuration - Add comprehensive user and directory permission setup --- playbooks/server-install.sh | 225 +++++++++++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 2 deletions(-) diff --git a/playbooks/server-install.sh b/playbooks/server-install.sh index 91cdba1f..987278cf 100644 --- a/playbooks/server-install.sh +++ b/playbooks/server-install.sh @@ -29,6 +29,7 @@ export DEBIAN_FRONTEND=noninteractive [[ -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 # @@ -345,6 +346,219 @@ 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() { + local php_fpm_user + php_fpm_user=$(get_php_fpm_user) + + # 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 PHP-FPM user to deployer group so it can access files + 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 + local php_fpm_service + php_fpm_service=$(get_php_fpm_service) + 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" + fi +} + +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() { + if [[ $DEPLOYER_FAMILY == 'debian' ]]; then + echo 'php8.4-fpm' + else + echo 'php-fpm' + 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 +586,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 +610,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 +619,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 From d3f6d4cab781a377d6613e8f4498980df29c9921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sat, 8 Nov 2025 22:21:08 +0200 Subject: [PATCH 4/9] feat: display deploy public key after server installation - Show generated deploy public key for Git provider configuration - Pass server name to installation playbook for SSH key comments - Improve user experience with clear next steps after installation --- app/Console/Server/ServerInstallCommand.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index abd9130f..8149fc52 100644 --- a/app/Console/Server/ServerInstallCommand.php +++ b/app/Console/Server/ServerInstallCommand.php @@ -100,6 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'DEPLOYER_DISTRO' => $distro, 'DEPLOYER_FAMILY' => $family, 'DEPLOYER_PERMS' => $permissions, + 'DEPLOYER_SERVER_NAME' => $server->name, ], true ); @@ -112,6 +113,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Server installed successfully'); + // Display deploy public key + if (isset($result['deploy_public_key']) && is_string($result['deploy_public_key']) && $result['deploy_public_key'] !== 'unknown') { + $this->io->writeln(''); + $this->io->writeln('Deploy Public Key:'); + $this->io->writeln('Add this key to your Git provider (GitHub, GitLab, etc.) to enable deployments:'); + $this->io->writeln(''); + $this->io->writeln('' . $result['deploy_public_key'] . ''); + $this->io->writeln(''); + } + // // Setup demo site // ---- From 70c68443ff311d4a22e57da08011461558cb8066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sun, 9 Nov 2025 11:43:47 +0200 Subject: [PATCH 5/9] feat(playbooks): simplify for Ubuntu/Debian-only support - Consolidate server-install.sh by removing duplicate code between Ubuntu/Debian branches - Remove DEPLOYER_FAMILY environment variable and related helper functions - Update playbook documentation to clarify Ubuntu/Debian-only support - Hardcode debian family values (www-data user, php8.4-fpm service) - Remove distribution family branching from demo-site.sh - Update PlaybooksTrait documentation --- app/Console/Server/ServerInstallCommand.php | 4 - app/Traits/PlaybooksTrait.php | 3 +- playbooks/demo-site.sh | 17 +- playbooks/server-install.sh | 279 +++++++------------- 4 files changed, 107 insertions(+), 196 deletions(-) diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index 8149fc52..2b4813ca 100644 --- a/app/Console/Server/ServerInstallCommand.php +++ b/app/Console/Server/ServerInstallCommand.php @@ -86,8 +86,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $family = $distribution->family()->value; - // // Execute installation playbook // --- @@ -98,7 +96,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'Installing server...', [ 'DEPLOYER_DISTRO' => $distro, - 'DEPLOYER_FAMILY' => $family, 'DEPLOYER_PERMS' => $permissions, 'DEPLOYER_SERVER_NAME' => $server->name, ], @@ -132,7 +129,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'demo-site', 'Setting up demo site...', [ - 'DEPLOYER_FAMILY' => $family, 'DEPLOYER_PERMS' => $permissions, ], true 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/playbooks/demo-site.sh b/playbooks/demo-site.sh index b6f49498..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 # # 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 @@ -109,13 +109,8 @@ setup_demo_site() { configure_caddy() { echo "✓ Configuring Caddy..." - # Determine PHP-FPM socket path - local php_fpm_socket - if [[ $DEPLOYER_FAMILY == 'debian' ]]; then - php_fpm_socket='/run/php/php8.4-fpm.sock' - else - php_fpm_socket='/run/php-fpm/www.sock' - fi + # PHP-FPM socket path (debian family) + local php_fpm_socket='/run/php/php8.4-fpm.sock' # Create log directory if [[ ! -d /var/log/caddy ]]; then diff --git a/playbooks/server-install.sh b/playbooks/server-install.sh index 987278cf..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,7 +31,6 @@ 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 @@ -127,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 @@ -171,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 @@ -259,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..." @@ -360,9 +313,6 @@ ensure_deployer_user() { } configure_deployer_groups() { - local php_fpm_user - php_fpm_user=$(get_php_fpm_user) - # 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..." @@ -381,55 +331,26 @@ configure_deployer_groups() { fi fi - # Add PHP-FPM user to deployer group so it can access files - 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 + # 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 - local php_fpm_service - php_fpm_service=$(get_php_fpm_service) - if systemctl is-active --quiet "$php_fpm_service" 2> /dev/null; then + 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 "$php_fpm_service"; then + 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 '$php_fpm_user' not found, skipping group assignment" - fi -} - -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() { - if [[ $DEPLOYER_FAMILY == 'debian' ]]; then - echo 'php8.4-fpm' - else - echo 'php-fpm' + echo "Warning: PHP-FPM user 'www-data' not found, skipping group assignment" fi } From b57d537511e71487517b19f58b691c8da09364aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sun, 9 Nov 2025 12:26:44 +0200 Subject: [PATCH 6/9] feat(servers): add permissions validation to server info Add validateServerPermissions() method to ServersTrait and integrate it into getServerInfo() to ensure servers have root or sudo permissions before proceeding with operations that require elevated privileges. --- app/Traits/ServersTrait.php | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/app/Traits/ServersTrait.php b/app/Traits/ServersTrait.php index 4bad86d5..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. * From ddc4a29d2f8069eedac7bd58a2882f946a41ef59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sun, 9 Nov 2025 12:26:46 +0200 Subject: [PATCH 7/9] refactor(server:install): use centralized validation from ServersTrait Remove duplicate distribution and permissions validation code from ServerInstallCommand. Now uses getServerInfo() which handles both validations centrally, reducing code duplication and improving maintainability. --- app/Console/Server/ServerInstallCommand.php | 71 +++++++++------------ 1 file changed, 30 insertions(+), 41 deletions(-) diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index 2b4813ca..4bc5dd05 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,29 +67,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $info; } - // - // Validate server 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; - } + [ + 'distro' => $distro, + 'permissions' => $permissions, + ] = $info; // // Execute installation playbook - // --- + // ---- $result = $this->executePlaybook( $server, @@ -110,16 +96,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->yay('Server installed successfully'); - // Display deploy public key - if (isset($result['deploy_public_key']) && is_string($result['deploy_public_key']) && $result['deploy_public_key'] !== 'unknown') { - $this->io->writeln(''); - $this->io->writeln('Deploy Public Key:'); - $this->io->writeln('Add this key to your Git provider (GitHub, GitLab, etc.) to enable deployments:'); - $this->io->writeln(''); - $this->io->writeln('' . $result['deploy_public_key'] . ''); - $this->io->writeln(''); - } - // // Setup demo site // ---- @@ -147,8 +123,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...' ); @@ -182,7 +162,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([ @@ -210,15 +190,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 [ From a05dc1394bfb890fb29b8174a6e6c329a8b44473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sun, 9 Nov 2025 12:26:55 +0200 Subject: [PATCH 8/9] docs(servers): update comments to reflect permissions validation Update comments across server commands to mention that getServerInfo() validates both distribution and permissions, not just distribution. --- app/Console/Server/ServerAddCommand.php | 2 +- app/Console/Server/ServerInfoCommand.php | 2 +- app/Console/Server/ServerLogsCommand.php | 2 +- app/Console/Server/ServerProvisionDigitalOceanCommand.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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)) { From db3b410412a315f8eea8e6530871d2702710ed86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucian=20V=C4=83c=C4=83roiu?= Date: Sun, 9 Nov 2025 12:32:56 +0200 Subject: [PATCH 9/9] fix: phpstan --- app/Console/Server/ServerInstallCommand.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Console/Server/ServerInstallCommand.php b/app/Console/Server/ServerInstallCommand.php index 4bc5dd05..bf35c606 100644 --- a/app/Console/Server/ServerInstallCommand.php +++ b/app/Console/Server/ServerInstallCommand.php @@ -72,6 +72,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'permissions' => $permissions, ] = $info; + /** @var string $distro */ + /** @var string $permissions */ + // // Execute installation playbook // ---- @@ -100,6 +103,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Setup demo site // ---- + /** @var string $permissions */ $demoResult = $this->executePlaybook( $server, 'demo-site',