diff --git a/recipe/provision.php b/recipe/provision.php index 862a48e2f..500eebaed 100644 --- a/recipe/provision.php +++ b/recipe/provision.php @@ -5,6 +5,7 @@ require __DIR__ . '/provision/databases.php'; require __DIR__ . '/provision/nodejs.php'; require __DIR__ . '/provision/php.php'; +require __DIR__ . '/provision/user.php'; require __DIR__ . '/provision/website.php'; use Deployer\Task\Context; @@ -27,12 +28,12 @@ 'provision:install', 'provision:ssh', 'provision:firewall', - 'provision:deployer', - 'provision:server', + 'provision:user', 'provision:php', + 'provision:node', 'provision:databases', 'provision:composer', - 'provision:npm', + 'provision:server', 'provision:website', 'provision:verify', ]); @@ -169,67 +170,6 @@ ->verbose() ->oncePerNode(); -desc('Configures a server'); -task('provision:server', function () { - run('usermod -a -G www-data caddy'); - $html = <<<'HTML' - - - - - - 404 Not Found - - - -
- - Deployer - - - - - - - -

Not Found

-

The requested URL was not found on this server.

-
- - - HTML; - run("mkdir -p /var/dep/html"); - run("echo $'$html' > /var/dep/html/404.html"); -})->oncePerNode(); - desc('Configures the ssh'); task('provision:ssh', function () { run("sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config"); @@ -241,61 +181,6 @@ } })->oncePerNode(); -set('sudo_password', function () { - return askHiddenResponse(' Password for sudo: '); -}); - -// Specify which key to copy to server. -// Set to `false` to disable copy of key. -set('ssh_copy_id', '~/.ssh/id_rsa.pub'); - -desc('Setups a deployer user'); -task('provision:deployer', function () { - if (test('id deployer >/dev/null 2>&1')) { - // TODO: Check what created deployer user configured correctly. - // TODO: Update sudo_password of deployer user. - // TODO: Copy ssh_copy_id to deployer ssh dir. - info('deployer user already exist'); - } else { - run('useradd deployer'); - run('mkdir -p /home/deployer/.ssh'); - run('mkdir -p /home/deployer/.deployer'); - run('adduser deployer sudo'); - - run('chsh -s /bin/bash deployer'); - run('cp /root/.profile /home/deployer/.profile'); - run('cp /root/.bashrc /home/deployer/.bashrc'); - - // Make color prompt. - run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc"); - - $password = run("mkpasswd -m sha-512 '%secret%'", ['secret' => get('sudo_password')]); - run("usermod --password '%secret%' deployer", ['secret' => $password]); - - if (!empty(get('ssh_copy_id'))) { - $file = parse_home_dir(get('ssh_copy_id')); - if (!file_exists($file)) { - info('Configure path to your public key.'); - writeln(""); - writeln(" set('ssh_copy_id', '~/.ssh/id_rsa.pub');"); - writeln(""); - $file = ask(' Specify path to your public ssh key: ', '~/.ssh/id_rsa.pub'); - } - run('echo "$KEY" >> /root/.ssh/authorized_keys', ['env' => ['KEY' => file_get_contents(parse_home_dir($file))]]); - } - run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys'); - run('ssh-keygen -f /home/deployer/.ssh/id_rsa -t rsa -N ""'); - - run('chown -R deployer:deployer /home/deployer'); - run('chmod -R 755 /home/deployer'); - run('chmod 700 /home/deployer/.ssh/id_rsa'); - - run('usermod -a -G www-data deployer'); - run('usermod -a -G caddy deployer'); - run('groups deployer'); - } -})->oncePerNode(); - desc('Setups a firewall'); task('provision:firewall', function () { run('ufw allow 22'); diff --git a/recipe/provision/nodejs.php b/recipe/provision/nodejs.php index 402740f8c..96add6c90 100644 --- a/recipe/provision/nodejs.php +++ b/recipe/provision/nodejs.php @@ -2,12 +2,34 @@ namespace Deployer; -// Node.js version from https://github.com/nodesource/distributions. -set('nodejs_version', 'node_23.x'); +use function Deployer\Support\starts_with; + +set('node_version', '23.x'); desc('Installs npm packages'); -task('provision:npm', function () { - run('npm install -g fx zx pm2'); - run('pm2 startup'); +task('provision:node', function () { + if (has('nodejs_version')) { + throw new \RuntimeException('nodejs_version is deprecated, use node_version_version instead.'); + } + $arch = run('uname -m'); + + if ($arch === 'arm' || starts_with($arch, 'armv7')) { + $filename = 'fnm-arm32'; + } elseif (starts_with($arch, 'aarch') || starts_with($arch, 'armv8')) { + $filename = 'fnm-arm64'; + } else { + $filename = 'fnm-linux'; + } + + $url = "https://github.com/Schniz/fnm/releases/latest/download/$filename.zip"; + run("curl -sSL $url --output /tmp/$filename.zip"); + + run("unzip /tmp/$filename.zip -d /tmp"); + + run("mv /tmp/fnm /usr/local/bin/fnm"); + run('chmod +x /usr/local/bin/fnm'); + + run('fnm install {{node_version}}'); + appendToFile('~/.bashrc', 'eval "`fnm env`"'); }) ->oncePerNode(); diff --git a/recipe/provision/php.php b/recipe/provision/php.php index 72c1af900..26f01683e 100644 --- a/recipe/provision/php.php +++ b/recipe/provision/php.php @@ -34,11 +34,11 @@ run('apt-get install -y ' . implode(' ', $packages), ['env' => ['DEBIAN_FRONTEND' => 'noninteractive']]); // Configure PHP-CLI - run("sudo sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini"); - run("sudo sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini"); - run("sudo sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini"); - run("sudo sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini"); - run("sudo sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini"); + run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini"); // Configure PHP-FPM run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/fpm/php.ini"); diff --git a/recipe/provision/user.php b/recipe/provision/user.php new file mode 100644 index 000000000..997838953 --- /dev/null +++ b/recipe/provision/user.php @@ -0,0 +1,85 @@ +/dev/null 2>&1')) { + // TODO: Check what created deployer user configured correctly. + // TODO: Update sudo_password of deployer user. + // TODO: Copy ssh_copy_id to deployer ssh dir. + info('deployer user already exist'); + } else { + run('useradd deployer'); + run('mkdir -p /home/deployer/.ssh'); + run('mkdir -p /home/deployer/.deployer'); + run('adduser deployer sudo'); + + run('chsh -s /bin/bash deployer'); + run('cp /root/.profile /home/deployer/.profile'); + run('cp /root/.bashrc /home/deployer/.bashrc'); + + // Make color prompt. + run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc"); + + $password = run("mkpasswd -m sha-512 '%secret%'", ['secret' => get('sudo_password')]); + run("usermod --password '%secret%' deployer", ['secret' => $password]); + + // Copy root public key to deployer user so user can login without password. + run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys'); + + // Create ssh key if not already exists. + run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); + + run('chown -R deployer:deployer /home/deployer'); + run('chmod -R 755 /home/deployer'); + run('chmod 700 /home/deployer/.ssh/id_ed25519'); + + run('usermod -a -G www-data deployer'); + run('usermod -a -G caddy deployer'); + run('groups deployer'); + } +})->oncePerNode(); + + +desc('Copy public key to remote server'); +task('provision:ssh_copy_id', function () { + $defaultKeys = [ + '~/.ssh/id_rsa.pub', + '~/.ssh/id_ed25519.pub', + '~/.ssh/id_ecdsa.pub', + '~/.ssh/id_dsa.pub', + ]; + + $publicKeyContent = false; + foreach ($defaultKeys as $key) { + $file = parse_home_dir($key); + if (file_exists($file)) { + $publicKeyContent = file_get_contents($file); + break; + } + } + + if (!$publicKeyContent) { + $publicKeyContent = ask(' Public key: ', false); + } + + if (!$publicKeyContent) { + info('Skipping public key copy as no public key was found or provided.'); + return; + } + + run('echo "$PUBLIC_KEY" >> /home/deployer/.ssh/authorized_keys', [ + 'env' => [ + 'PUBLIC_KEY' => $publicKeyContent, + ], + ]); +}); diff --git a/recipe/provision/website.php b/recipe/provision/website.php index bfdad9cf0..2596550d6 100644 --- a/recipe/provision/website.php +++ b/recipe/provision/website.php @@ -12,8 +12,71 @@ return ask(' Public path: ', 'public'); }); +desc('Configures a server'); +task('provision:server', function () { + run('usermod -a -G www-data caddy'); + $html = <<<'HTML' + + + + + + 404 Not Found + + + +
+ + Deployer + + + + + + + +

Not Found

+

The requested URL was not found on this server.

+
+ + + HTML; + run("mkdir -p /var/deployer"); + run("echo $'$html' > /var/deployer/404.html"); +})->oncePerNode(); + desc('Provision website'); task('provision:website', function () { + $restoreBecome = become('deployer'); + run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}"); run("chown -R deployer:deployer {{deploy_path}}"); @@ -44,6 +107,8 @@ run("echo $'$caddyfile' > Caddyfile"); } + $restoreBecome(); + if (!test("grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile")) { run("echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile"); } @@ -52,12 +117,12 @@ info("Website {{domain}} configured!"); })->limit(1); -desc('Shows caddy logs'); -task('logs:caddy', function () { +desc('Shows access logs'); +task('logs:access', function () { run('tail -f {{deploy_path}}/log/access.log'); })->verbose(); desc('Shows caddy syslog'); -task('logs:caddy:syslog', function () { +task('logs:caddy', function () { run('sudo journalctl -u caddy -f'); })->verbose(); diff --git a/src/Component/Ssh/Client.php b/src/Component/Ssh/Client.php index 71b3e827c..358686fae 100644 --- a/src/Component/Ssh/Client.php +++ b/src/Component/Ssh/Client.php @@ -61,7 +61,7 @@ public function run(Host $host, string $command, array $config = []): string $shellId = bin2hex(random_bytes(10)); $shellCommand = $host->getShell(); - if ($host->has('become')) { + if ($host->has('become') && !empty($host->get('become'))) { $shellCommand = "sudo -H -u {$host->get('become')} " . $shellCommand; } diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 7b4ca2daf..1f07855c3 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -76,10 +76,10 @@ function str_contains(string $haystack, string $needle): bool /** * Checks if string stars with given prefix. */ -function starts_with(string $string, string $startString): bool +function starts_with(string $string, string $prefix): bool { - $len = strlen($startString); - return (substr($string, 0, $len) === $startString); + $len = strlen($prefix); + return (substr($string, 0, $len) === $prefix); } /** diff --git a/src/functions.php b/src/functions.php index 0a286a2e2..f95978bc4 100644 --- a/src/functions.php +++ b/src/functions.php @@ -34,6 +34,7 @@ use function Deployer\Support\array_merge_alternate; use function Deployer\Support\env_stringify; +use function Deployer\Support\escape_shell_argument; use function Deployer\Support\is_closure; use function Deployer\Support\str_contains; @@ -312,10 +313,36 @@ function cd(string $path): void set('working_path', parse($path)); } +/** + * Change the current user. + * + * Usage: + * ```php + * $restore = become('deployer'); + * + * // do something + * + * $restore(); // revert back to the previous user + * ``` + * + * @param string $user + * @return \Closure + * @throws Exception + */ +function become(string $user): \Closure +{ + $currentBecome = get('become'); + set('become', $user); + return function () use ($currentBecome) { + set('become', $currentBecome); + }; +} + /** * Execute a callback within a specific directory and revert back to the initial working directory. * * @return mixed|null Return value of the $callback function or null if callback doesn't return anything + * @throws Exception */ function within(string $path, callable $callback) { @@ -326,8 +353,6 @@ function within(string $path, callable $callback) } finally { set('working_path', $lastWorkingPath); } - - return null; } /** @@ -826,7 +851,7 @@ function askHiddenResponse(string $message): string } if (Deployer::isWorker()) { - return (string) Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args()); + return (string)Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args()); } /** @var QuestionHelper */ @@ -840,7 +865,7 @@ function askHiddenResponse(string $message): string $question->setHidden(true); $question->setHiddenFallback(false); - return (string) $helper->ask(input(), output(), $question); + return (string)$helper->ask(input(), output(), $question); } function input(): InputInterface @@ -955,3 +980,18 @@ function fetch(string $url, string $method = 'get', array $headers = [], ?string } return $http->send($info); } + + +/** + * Appends a string to a file. + * + * @param string $file + * @param string $string + * @throws Exception + * @throws RunException + * @throws TimeoutException + */ +function appendToFile(string $file, string $string): void +{ + run("echo " . escape_shell_argument($string) . " >> $file"); +}