438 lines
16 KiB
PHP
438 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Evoweb\DeployerConfig\Tasks;
|
|
|
|
use Deployer\Exception\ConfigurationException;
|
|
use Deployer\Exception\Exception;
|
|
use Deployer\Exception\GracefulShutdownException;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
|
|
use function Deployer\currentHost;
|
|
use function Deployer\error;
|
|
use function Deployer\get;
|
|
use function Deployer\info;
|
|
use function Deployer\output;
|
|
use function Deployer\parse;
|
|
use function Deployer\runLocally;
|
|
use function Deployer\set;
|
|
use function Deployer\Support\escape_shell_argument;
|
|
use function Deployer\testLocally;
|
|
use function Deployer\timestamp;
|
|
|
|
class Local extends AbstractTasks
|
|
{
|
|
protected array $tasks = [
|
|
'local:info',
|
|
'local:setup',
|
|
'local:lock',
|
|
'local:unlock',
|
|
'local:release',
|
|
'local:update_code',
|
|
'local:env',
|
|
'local:shared',
|
|
'local:writable',
|
|
'local:write_release',
|
|
'local:vendors',
|
|
'local:clear_paths',
|
|
'local:symlink',
|
|
'local:cleanup',
|
|
'local:echo_release_number',
|
|
];
|
|
|
|
/**
|
|
* Displays info about deployment
|
|
*/
|
|
public function info(): void
|
|
{
|
|
info("deploying <fg=green;options=bold>{{what}}</> to <fg=magenta;options=bold>{{where}}</>");
|
|
}
|
|
|
|
/**
|
|
* Prepares host for deploy
|
|
*/
|
|
public function setup(): void
|
|
{
|
|
runLocally(
|
|
<<<EOF
|
|
[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}};
|
|
cd {{deploy_path}};
|
|
[ -d .dep ] || mkdir .dep;
|
|
[ -d releases ] || mkdir releases;
|
|
[ -d shared ] || mkdir shared;
|
|
EOF
|
|
);
|
|
|
|
// If current_path points to something like "/var/www/html", make sure it is
|
|
// a symlink and not a directory.
|
|
if (testLocally('[ ! -L {{current_path}} ] && [ -d {{current_path}} ]')) {
|
|
throw error("There is a directory (not symlink) at {{current_path}}.\n Remove this directory so it can be replaced with a symlink for atomic deployments.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Locks deploy
|
|
*/
|
|
public function lock(): void
|
|
{
|
|
$user = escapeshellarg(get('user'));
|
|
$locked = runLocally("[ -f {{deploy_path}}/.dep/deploy.lock ] && echo +locked || echo $user > {{deploy_path}}/.dep/deploy.lock");
|
|
if ($locked === '+locked') {
|
|
$lockedUser = runLocally("cat {{deploy_path}}/.dep/deploy.lock");
|
|
throw new GracefulShutdownException(
|
|
"Deploy locked by $lockedUser.\n" .
|
|
"Execute \"deploy:unlock\" task to unlock."
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepares release
|
|
*/
|
|
public function release(): void
|
|
{
|
|
// Clean up if there is unfinished release.
|
|
if (testLocally('[ -h {{deploy_path}}/release ]')) {
|
|
runLocally('rm {{deploy_path}}/release'); // Delete symlink.
|
|
}
|
|
|
|
// We need to get releases_list at same point as release_name,
|
|
// as standard release_name's implementation depends on it and,
|
|
// if user overrides it, we need to get releases_list manually.
|
|
$releasesList = get('releases_list');
|
|
$releaseName = get('release_name');
|
|
$releasePath = "{{deploy_path}}/releases/$releaseName";
|
|
|
|
// Check what there is no such release path.
|
|
if (testLocally("[ -d $releasePath ]")) {
|
|
$freeReleaseName = '...';
|
|
// Check what $releaseName is integer.
|
|
if (ctype_digit($releaseName)) {
|
|
$freeReleaseName = intval($releaseName);
|
|
// Find free release name.
|
|
while (testLocally("[ -d {{deploy_path}}/releases/$freeReleaseName ]")) {
|
|
$freeReleaseName++;
|
|
}
|
|
}
|
|
throw new Exception("Release name \"$releaseName\" already exists.\nRelease name can be overridden via:\n dep deploy -o release_name=$freeReleaseName");
|
|
}
|
|
|
|
// Save release_name.
|
|
if (is_numeric($releaseName) && is_integer(intval($releaseName))) {
|
|
runLocally("echo $releaseName > {{deploy_path}}/.dep/latest_release");
|
|
}
|
|
|
|
// Metainfo.
|
|
$timestamp = timestamp();
|
|
$metainfo = [
|
|
'created_at' => $timestamp,
|
|
'release_name' => $releaseName,
|
|
'user' => get('user'),
|
|
'target' => get('target'),
|
|
];
|
|
|
|
// Save metainfo about release.
|
|
$json = escape_shell_argument(json_encode($metainfo));
|
|
runLocally("echo $json >> {{deploy_path}}/.dep/releases_log");
|
|
|
|
// Make new release.
|
|
runLocally("mkdir -p $releasePath");
|
|
runLocally("{{bin/symlink}} $releasePath {{deploy_path}}/release");
|
|
|
|
// Add to releases list.
|
|
array_unshift($releasesList, $releaseName);
|
|
set('releases_list', $releasesList);
|
|
|
|
// Set previous_release.
|
|
if (isset($releasesList[1])) {
|
|
set('previous_release', "{{deploy_path}}/releases/{$releasesList[1]}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates code
|
|
*/
|
|
public function updateCode(): void
|
|
{
|
|
$git = get('bin/git');
|
|
$repository = get('repository');
|
|
$target = get('target');
|
|
|
|
if (empty($repository)) {
|
|
throw new ConfigurationException("Missing 'repository' configuration.");
|
|
}
|
|
|
|
// Copy to release_path.
|
|
runLocally("cd {{release_path}}; $git clone --branch $target $repository .");
|
|
|
|
// Save git revision in REVISION file.
|
|
$rev = escapeshellarg(runLocally("cd {{release_path}}; $git rev-list $target -1"));
|
|
runLocally("echo $rev > {{release_path}}/REVISION");
|
|
}
|
|
|
|
/**
|
|
* Configure .env file
|
|
*/
|
|
public function env(): void
|
|
{
|
|
if (testLocally('[ ! -e {{release_or_current_path}}/.env ] && [ -f {{release_or_current_path}}/{{dotenv_example}} ]')) {
|
|
runLocally('cp {{release_or_current_path}}/{{dotenv_example}} {{release_or_current_path}}/.env');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates symlinks for shared files and dirs
|
|
*/
|
|
public function shared(): void
|
|
{
|
|
$sharedPath = "{{deploy_path}}/shared";
|
|
|
|
// Validate shared_dir, find duplicates
|
|
foreach (get('shared_dirs') as $a) {
|
|
foreach (get('shared_dirs') as $b) {
|
|
if ($a !== $b && strpos(rtrim($a, '/') . '/', rtrim($b, '/') . '/') === 0) {
|
|
throw new Exception("Can not share same dirs `$a` and `$b`.");
|
|
}
|
|
}
|
|
}
|
|
|
|
$copyVerbosity = output()->getVerbosity() === OutputInterface::VERBOSITY_DEBUG ? 'v' : '';
|
|
|
|
foreach (get('shared_dirs') as $dir) {
|
|
// Make sure all path without tailing slash.
|
|
$dir = trim($dir, '/');
|
|
|
|
// Check if shared dir does not exist.
|
|
if (!testLocally("[ -d $sharedPath/$dir ]")) {
|
|
// Create shared dir if it does not exist.
|
|
runLocally("mkdir -p $sharedPath/$dir");
|
|
// If release contains shared dir, copy that dir from release to shared.
|
|
if (testLocally("[ -d $(echo {{release_path}}/$dir) ]")) {
|
|
runLocally("cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/" . dirname($dir));
|
|
}
|
|
}
|
|
|
|
// Remove from source.
|
|
runLocally("rm -rf {{release_path}}/$dir");
|
|
|
|
// Create path to shared dir in release dir if it does not exist.
|
|
// Symlink will not create the path and will fail otherwise.
|
|
runLocally("mkdir -p `dirname {{release_path}}/$dir`");
|
|
|
|
// Symlink shared dir to release dir
|
|
runLocally("{{bin/symlink}} $sharedPath/$dir {{release_path}}/$dir");
|
|
}
|
|
|
|
foreach (get('shared_files') as $file) {
|
|
$dirname = dirname(parse($file));
|
|
|
|
// Create dir of shared file if not existing
|
|
if (!testLocally("[ -d $sharedPath/$dirname ]")) {
|
|
runLocally("mkdir -p $sharedPath/$dirname");
|
|
}
|
|
|
|
// Check if shared file does not exist in shared.
|
|
// and file exist in release
|
|
if (!testLocally("[ -f $sharedPath/$file ]") && testLocally("[ -f {{release_path}}/$file ]")) {
|
|
// Copy file in shared dir if not present
|
|
runLocally("cp -r$copyVerbosity {{release_path}}/$file $sharedPath/$file");
|
|
}
|
|
|
|
// Remove from source.
|
|
runLocally("if [ -f $(echo {{release_path}}/$file) ]; then rm -rf {{release_path}}/$file; fi");
|
|
|
|
// Ensure dir is available in release
|
|
runLocally("if [ ! -d $(echo {{release_path}}/$dirname) ]; then mkdir -p {{release_path}}/$dirname;fi");
|
|
|
|
// Touch shared
|
|
runLocally("[ -f $sharedPath/$file ] || touch $sharedPath/$file");
|
|
|
|
// Symlink shared dir to release dir
|
|
runLocally("{{bin/symlink}} $sharedPath/$file {{release_path}}/$file");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Makes writable dirs
|
|
*/
|
|
public function writable(): void
|
|
{
|
|
$dirs = join(' ', get('writable_dirs'));
|
|
$mode = get('writable_mode');
|
|
$recursive = get('writable_recursive') ? '-R' : '';
|
|
$sudo = get('writable_use_sudo') ? 'sudo' : '';
|
|
|
|
if (empty($dirs)) {
|
|
return;
|
|
}
|
|
// Check that we don't have absolute path
|
|
if (strpos($dirs, ' /') !== false) {
|
|
throw new \RuntimeException('Absolute path not allowed in config parameter `writable_dirs`.');
|
|
}
|
|
|
|
$options = ['cwd' => $this->get('release_or_current_path')];
|
|
|
|
// Create directories if they don't exist
|
|
runLocally("mkdir -p $dirs", $options);
|
|
|
|
if ($mode === 'chown') {
|
|
$httpUser = get('http_user');
|
|
// Change owner.
|
|
// -L traverse every symbolic link to a directory encountered
|
|
runLocally("$sudo chown -L $recursive $httpUser $dirs", $options);
|
|
} elseif ($mode === 'chgrp') {
|
|
// Change group ownership.
|
|
// -L traverse every symbolic link to a directory encountered
|
|
runLocally("$sudo chgrp -L $recursive {{http_group}} $dirs", $options);
|
|
runLocally("$sudo chmod $recursive g+rwx $dirs", $options);
|
|
} elseif ($mode === 'chmod') {
|
|
runLocally("$sudo chmod $recursive {{writable_chmod_mode}} $dirs", $options);
|
|
} elseif ($mode === 'acl') {
|
|
$remoteUser = get('remote_user', false);
|
|
if (empty($remoteUser)) {
|
|
$remoteUser = runLocally('whoami');
|
|
}
|
|
$httpUser = get('http_user');
|
|
if (strlen(runLocally("chmod --help | grep ugoa; true")) > 0) {
|
|
// Try OS-X specific setting of access-rights
|
|
|
|
runLocally("$sudo chmod g+w $dirs", $options);
|
|
} elseif ($this->commandExist('setfacl')) {
|
|
$setFaclUsers = "-m u:\"$httpUser\":rwX";
|
|
// Check if remote user exists, before adding it to setfacl
|
|
$remoteUserExists = testLocally("id -u $remoteUser &>/dev/null 2>&1 || exit 0");
|
|
if ($remoteUserExists === true) {
|
|
$setFaclUsers .= " -m u:$remoteUser:rwX";
|
|
}
|
|
if (empty($sudo)) {
|
|
// When running without sudo, exception may be thrown
|
|
// if executing setfacl on files created by http user (in directory that has been setfacl before).
|
|
// These directories/files should be skipped.
|
|
// Now, we will check each directory for ACL and only setfacl for which has not been set before.
|
|
$writeableDirs = get('writable_dirs');
|
|
foreach ($writeableDirs as $dir) {
|
|
// Check if ACL has been set or not
|
|
$hasfacl = runLocally("getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l", $options);
|
|
// Set ACL for directory if it has not been set before
|
|
if (!$hasfacl) {
|
|
runLocally("setfacl -L $recursive $setFaclUsers $dir", $options);
|
|
runLocally("setfacl -dL $recursive $setFaclUsers $dir", $options);
|
|
}
|
|
}
|
|
} else {
|
|
runLocally("$sudo setfacl -L $recursive $setFaclUsers $dirs", $options);
|
|
runLocally("$sudo setfacl -dL $recursive $setFaclUsers $dirs", $options);
|
|
}
|
|
} else {
|
|
$alias = currentHost()->getAlias();
|
|
throw new \RuntimeException("Can't set writable dirs with ACL.\nInstall ACL with next command:\ndep run 'sudo apt-get install acl' -- $alias");
|
|
}
|
|
} elseif ($mode === 'sticky') {
|
|
// Changes the group of the files, sets sticky bit to the directories
|
|
// and add the writable bit for all files
|
|
runLocally("for dir in $dirs;" .
|
|
'do ' .
|
|
'chgrp -L -R {{http_group}} ${dir}; ' .
|
|
'find ${dir} -type d -exec chmod g+rwxs \{\} \;;' .
|
|
'find ${dir} -type f -exec chmod g+rw \{\} \;;' .
|
|
'done', $options);
|
|
} elseif ($mode === 'skip') {
|
|
// Does nothing, saves time if no changes are required for some environments
|
|
return;
|
|
} else {
|
|
throw new \RuntimeException("Unknown writable_mode `$mode`.");
|
|
}
|
|
}
|
|
|
|
public function writeRelease(): void
|
|
{
|
|
runLocally('echo {{target}} > {{release_path}}/release');
|
|
}
|
|
|
|
/**
|
|
* Installing vendors
|
|
*/
|
|
public function vendors(): void
|
|
{
|
|
if (!$this->commandExist('unzip')) {
|
|
info(
|
|
'<comment>To speed up composer installation setup "unzip" command' .
|
|
' with PHP zip extension https://goo.gl/sxzFcD</comment>'
|
|
);
|
|
}
|
|
|
|
// if composer.json exists
|
|
if (testLocally('[ -d $(echo {{release_path}}) ] && [ -e "{{release_path}}/composer.json" ]')) {
|
|
runLocally('{{bin/composer}} --working-dir={{release_path}} {{composer_options}};');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup files and/or directories
|
|
*/
|
|
public function clearPaths(): void
|
|
{
|
|
$paths = get('clear_paths');
|
|
$sudo = get('clear_use_sudo') ? 'sudo' : '';
|
|
$batch = 100;
|
|
|
|
$commands = [];
|
|
foreach ($paths as $path) {
|
|
$commands[] = "$sudo rm -rf {{release_path}}/$path";
|
|
}
|
|
$chunks = array_chunk($commands, $batch);
|
|
foreach ($chunks as $chunk) {
|
|
runLocally(implode('; ', $chunk));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creating symlink to release
|
|
*/
|
|
public function symlink(): void
|
|
{
|
|
if (get('use_atomic_symlink')) {
|
|
runLocally("mv -T {{deploy_path}}/release {{current_path}}");
|
|
} else {
|
|
// Atomic symlink does not supported.
|
|
// Will use simple two steps switch.
|
|
|
|
runLocally("cd {{deploy_path}} && {{bin/symlink}} {{release_path}} {{current_path}}"); // Atomic override symlink.
|
|
runLocally("cd {{deploy_path}} && rm release"); // Remove release link.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleaning up old releases
|
|
*/
|
|
public function cleanup(): void
|
|
{
|
|
$releases = get('releases_list');
|
|
$keep = get('keep_releases');
|
|
$sudo = get('cleanup_use_sudo') ? 'sudo' : '';
|
|
|
|
runLocally("cd {{deploy_path}} && if [ -e release ]; then rm release; fi");
|
|
|
|
if ($keep > 0) {
|
|
foreach (array_slice($releases, $keep) as $release) {
|
|
runLocally("$sudo rm -rf {{deploy_path}}/releases/$release");
|
|
}
|
|
}
|
|
}
|
|
|
|
public function echoReleaseNumber(): void
|
|
{
|
|
info('Folder of this release is: {{release_path}}');
|
|
}
|
|
|
|
/**
|
|
* Unlocks deploy
|
|
*/
|
|
public function unlock(): void
|
|
{
|
|
// always success
|
|
runLocally("rm -f {{deploy_path}}/.dep/deploy.lock");
|
|
}
|
|
}
|