diff --git a/Classes/Config/Deployment.php b/Classes/Config/Deployment.php index d040ac3..41e361c 100644 --- a/Classes/Config/Deployment.php +++ b/Classes/Config/Deployment.php @@ -1,123 +1,45 @@ ['body' => 'localInfo'], - 'local:setup' => ['body' => 'localSetup'], - 'remote:prepare' => ['body' => 'remotePrepare'], - 'local:lock' => ['body' => 'localLock'], - 'local:unlock' => ['body' => 'localUnlock'], - 'local:release' => ['body' => 'localRelease'], - 'local:update_code' => ['body' => 'localUpdateCode'], - 'local:env' => ['body' => 'localEnv'], - 'local:shared' => ['body' => 'localShared'], - 'local:writable' => ['body' => 'localWritable'], - 'local:write_release' => ['body' => 'localWriteRelease'], - 'local:create_folder' => ['body' => 'localCreateFolder'], - 'local:vendors' => ['body' => 'localVendors'], - 'local:clear_paths' => ['body' => 'localClearPaths'], - 'local:symlink' => ['body' => 'localSymlink'], - 'local:cleanup' => ['body' => 'localCleanup'], - 'local:echo_release_number' => ['body' => 'echoReleaseNumber'], - 'remote:change_group' => ['body' => 'remoteChangeGroup'], - 'remote:writable' => ['body' => 'remoteWritable'], - 'remote:db_compare' => ['body' => 'remoteDbCompare'], - 'remote:fix_folder_structure' => ['body' => 'remoteFixFolderStructure'], - 'remote:clear_cache' => ['body' => 'remoteClearCache'], - 'remote:clear_opcache' => ['body' => 'remoteClearOpcache'], - - 'deploy:prepare' => [ - 'body' => [ - 'local:info', - 'local:setup', - 'remote:prepare', - 'rsync:warmup', - 'local:lock', - 'local:release', - 'local:update_code', - 'local:env', - 'local:shared', - 'local:writable', - 'local:write_release', - 'local:create_folder', - 'local:vendors', - ] - ], - - 'deploy:publish' => [ - 'body' => [ - 'local:clear_paths', - 'local:symlink', - 'local:cleanup', - 'rsync:remote', - 'remote:change_group', - 'remote:writable', - 'remote:db_compare', - 'remote:fix_folder_structure', - 'rsync:switch_current', - 'remote:clear_cache', - 'remote:clear_opcache', - 'local:echo_release_number', - 'local:unlock', - 'deploy:success' - ] - ], - ]; - - public function __construct(array $tasks = []) + public function __construct() { - if ($tasks !== []) { - $this->tasks = $tasks; - } $this->loadDeployerCommon(); $this->setDefaultConfiguration(); $this->loadHostConfiguration(); - $this->initializeTasks(); - $this->registerAfterTask(); + $this->registerTasks(); + } protected function loadDeployerCommon(): void { foreach (['/../../', '/../../../', '/../../../../'] as $path) { $file = realpath(__DIR__ . $path . 'vendor/deployer/deployer/recipe/common.php'); - if (file_exists($file)) { + if ($file && file_exists($file)) { require $file; break; } } - - new Rsync(); } protected function setDefaultConfiguration(): void @@ -229,7 +151,7 @@ class Deployment extends AbstractConfiguration $releaseExists = testLocally('[ -h {{deploy_path}}/release ]'); if ($releaseExists) { $link = runLocally("readlink {{deploy_path}}/release"); - return substr($link, 0, 1) === '/' ? $link : get('deploy_path') . '/' . $link; + return str_starts_with($link, '/') ? $link : get('deploy_path') . '/' . $link; } else { throw new Exception(parse('The "release_path" ({{deploy_path}}/release) does not exist.')); } @@ -245,7 +167,7 @@ class Deployment extends AbstractConfiguration $httpUser = array_shift($candidates); if (empty($httpUser)) { - throw new \RuntimeException( + throw new RuntimeException( "Can't detect http user name.\n" . "Please setup `http_user` config parameter." ); @@ -259,7 +181,7 @@ class Deployment extends AbstractConfiguration $httpGroup = array_shift($candidates); if (empty($httpGroup)) { - throw new \RuntimeException( + throw new RuntimeException( "Can't detect http user name.\n" . "Please setup `http_group` config parameter." ); @@ -281,42 +203,14 @@ class Deployment extends AbstractConfiguration } } - protected function registerAfterTask(): void + protected function registerTasks(): void { - // [Optional] if deploy fails automatically unlock. - after('deploy:failed', 'local:unlock'); + new Rsync(); + new Local(); + new Remote(); + new Deploy(); } - protected function getSudo(): string - { - return $this->get('clear_use_sudo') ? 'sudo ' : ''; - } - - protected function processRemoteTask(callable $callback): void - { - $config = Context::get()->getConfig(); - if ($config->get('local') && $config->get('remote_path')) { - // backup - $contextBackup = Context::pop(); - - $config->set('local', false); - - // create temporary host - $hostname = $contextBackup->getHost()->get('hostname'); - $host = new Host($hostname); - // create new context with host - $context = new Context($host); - Context::push($context); - - // execute the task - \Closure::bind($callback, $this)(); - - // restore - Context::pop(); - $config->set('local', true); - Context::push($contextBackup); - } - } protected function commandSupportsOption(string $command, string $option): bool { @@ -341,611 +235,15 @@ class Deployment extends AbstractConfiguration // Fallback to `type` command, if the rest fails $path = runLocally("command -v $nameEscaped || which $nameEscaped || type -p $nameEscaped"); if (empty($path)) { - throw new \RuntimeException("Can't locate [$nameEscaped] - neither of [command|which|type] commands are available"); + throw new RuntimeException("Can't locate [$nameEscaped] - neither of [command|which|type] commands are available"); } // Deal with issue when `type -p` outputs something like `type -ap` in some implementations return trim(str_replace("$name is", "", $path)); } - protected function getApplicationContext(): string + protected function set(string $name, $value): void { - return $this->has('application_context') ? - 'TYPO3_CONTEXT="' . $this->get('application_context') . '" ' : - ''; - } - - protected function getTYPO3Bin(string $path, string $testPath): string - { - if (test('[ -f ' . $testPath . '/releases/{{release_name}}/vendor/bin/typo3cms ]')) { - $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3cms'; - } else { - $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3'; - } - return $bin; - } - - - /** - * Displays info about deployment - */ - public function localInfo(): void - { - info("deploying {{what}} to {{where}}"); - } - - /** - * Prepares host for deploy - */ - public function localSetup(): void - { - runLocally( - <<hasArray('prepare_dirs')) { - return; - } - - $prepareDirs = $this->get('prepare_dirs'); - foreach ($prepareDirs as $dir) { - if (test('[ ! -e "{{remote_path}}/' . $dir . '" ]')) { - run($this->getSudo() . ' mkdir -p {{remote_path}}/' . $dir); - } - } - } - - /** - * Locks deploy - */ - public function localLock(): 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." - ); - } - } - - /** - * Unlocks deploy - */ - public function localUnlock(): void - { - // always success - runLocally("rm -f {{deploy_path}}/.dep/deploy.lock"); - } - - /** - * Prepares release - */ - public function localRelease(): 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 localUpdateCode(): void - { - $git = get('bin/git'); - $repository = get('repository'); - $target = get('target'); - - if (empty($repository)) { - throw new ConfigurationException("Missing 'repository' configuration."); - } - - $targetWithDir = $target; - if (!empty(get('sub_directory'))) { - $targetWithDir .= ':{{sub_directory}}'; - } - - $bare = parse('{{deploy_path}}/.dep/repo'); - $env = [ - 'GIT_TERMINAL_PROMPT' => '0', - 'GIT_SSH_COMMAND' => get('git_ssh_command'), - ]; - - start: - // Clone the repository to a bare repo. - runLocally("[ -d $bare ] || mkdir -p $bare"); - runLocally("[ -f $bare/HEAD ] || $git clone --mirror $repository $bare 2>&1", ['env' => $env]); - - // If remote url changed, drop `.dep/repo` and reinstall. - if (runLocally("cd $bare; $git config --get remote.origin.url") !== $repository) { - runLocally("rm -rf {{deploy_path}}$bare"); - goto start; - } - - runLocally("$git remote update 2>&1", ['env' => $env]); - - // Copy to release_path. - if (get('update_code_strategy') === 'archive') { - runLocally("cd $bare; $git archive $targetWithDir | tar -x -f - -C {{release_path}} 2>&1"); - } elseif (get('update_code_strategy') === 'clone') { - runLocally("cd {{release_path}}; $git clone -l $bare ."); - runLocally("cd {{release_path}}; $git remote set-url origin $repository", ['env' => $env]); - runLocally("cd {{release_path}}; $git checkout --force $target"); - } else { - throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); - } - - // Save git revision in REVISION file. - $rev = escapeshellarg(runLocally("cd $bare; $git rev-list $target -1")); - runLocally("echo $rev > {{release_path}}/REVISION"); - } - - /** - * Configure .env file - */ - public function localEnv(): 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 localShared(): 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 localWritable(): 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`.'); - } - - // Create directories if they don't exist - runLocally("cd {{release_or_current_path}}; mkdir -p $dirs"); - - if ($mode === 'chown') { - $httpUser = get('http_user'); - // Change owner. - // -L traverse every symbolic link to a directory encountered - runLocally("cd {{release_or_current_path}}; $sudo chown -L $recursive $httpUser $dirs"); - } elseif ($mode === 'chgrp') { - // Change group ownership. - // -L traverse every symbolic link to a directory encountered - runLocally("cd {{release_or_current_path}}; $sudo chgrp -L $recursive {{http_group}} $dirs"); - runLocally("cd {{release_or_current_path}}; $sudo chmod $recursive g+rwx $dirs"); - } elseif ($mode === 'chmod') { - runLocally("cd {{release_or_current_path}}; $sudo chmod $recursive {{writable_chmod_mode}} $dirs"); - } 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("cd {{release_or_current_path}}; $sudo chmod g+w $dirs"); - } 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("cd {{release_or_current_path}}; getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l"); - // Set ACL for directory if it has not been set before - if (!$hasfacl) { - runLocally("cd {{release_or_current_path}}; setfacl -L $recursive $setFaclUsers $dir"); - runLocally("cd {{release_or_current_path}}; setfacl -dL $recursive $setFaclUsers $dir"); - } - } - } else { - runLocally("cd {{release_or_current_path}}; $sudo setfacl -L $recursive $setFaclUsers $dirs"); - runLocally("cd {{release_or_current_path}}; $sudo setfacl -dL $recursive $setFaclUsers $dirs"); - } - } 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("cd {{release_or_current_path}}; 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'); - } 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 localWriteRelease(): void - { - runLocally('echo ' . getEnv('CI_COMMIT_REF_NAME') . ' > {{release_path}}/release'); - } - - /** - * Creating necessary folders - */ - public function localCreateFolder(): void - { - $sudo = $this->getSudo(); - - if (testLocally('[ ! -d {{release_path}}/var/cache ]')) { - runLocally('mkdir -p {{release_path}}/var/cache'); - } - runLocally($sudo . ' chmod -R 775 {{release_path}}/var/cache'); - - if (testLocally('[ ! -d {{release_path}}/var/log ]')) { - runLocally('mkdir -p {{release_path}}/var/log'); - } - runLocally($sudo . ' chmod -R 775 {{release_path}}/var/log'); - } - - /** - * Installing vendors - */ - public function localVendors(): void - { - if (!$this->commandExist('unzip')) { - info( - 'To speed up composer installation setup "unzip" command' . - ' with PHP zip extension https://goo.gl/sxzFcD' - ); - } - - // 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 localClearPaths(): 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 localSymlink(): 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 localCleanup(): 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"); - } - } - } - - /** - * Change usergroup for the hole project - */ - public function remoteChangeGroup(): void - { - if ($this->get('http_group')) { - $sudo = get('writable_use_sudo') ? 'sudo' : ''; - $runOpts = []; - if ($sudo) { - $runOpts['tty'] = $this->get('writable_tty', false); - } - - $path = parse('{{remote_path}}/releases/{{release_name}}/'); - run($sudo . ' chgrp -H -R ' . $this->get('http_group') . ' ' . $path, $runOpts); - run($sudo . ' chmod -R 2755 ' . $path, $runOpts); - } - } - - /** - * Set files and/or folders writable - */ - public function remoteWritable(): void - { - if ($this->hasArray('writable_dirs') || $this->hasArray('writable_files')) { - $sudo = get('writable_use_sudo') ? 'sudo' : ''; - $runOpts = []; - if ($sudo) { - $runOpts['tty'] = get('writable_tty', false); - } - $recursive = $this->get('writable_recursive') ? '-R' : ''; - $httpGroup = $this->get('http_group', false); - $path = parse('{{remote_path}}/releases/{{release_name}}/'); - - if ($this->hasArray('writable_dirs')) { - foreach ($this->get('writable_dirs') as $dir) { - if (test('[ ! -d "' . $path . $dir . '" ]')) { - run('mkdir -p ' . $path . $dir); - } - run($sudo . ' chmod -R 2775 ' . $path . $dir); - if ($httpGroup) { - run( - $sudo . ' chgrp -H ' . $recursive . ' ' . $httpGroup . ' ' . $path . $dir, - $runOpts - ); - } - } - } - - if ($this->hasArray('writable_files')) { - foreach ($this->get('writable_files') as $file) { - if (test('[ -e "' . $path . $file . '" ]')) { - run($sudo . ' chmod g+w ' . $path . $file); - if ($httpGroup) { - run($sudo . ' chgrp ' . $httpGroup . ' ' . $path . $file, $runOpts); - } - } - } - } - } - } - - /** - * Database migration - */ - public function remoteDbCompare(): void - { - $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); - - $php = $this->getSudo() . parse('{{bin/php}} '); - if (!str_contains($php, 'TYPO3_CONTEXT')) { - $php = $this->getApplicationContext() . $php; - } - - $result = run( - $php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' database:updateschema "*.add,*.change"' - ); - writeln($result); - } - - /** - * Fix folder structure - */ - public function remoteFixFolderStructure(): void - { - $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); - - $php = $this->getSudo() . parse('{{bin/php}} '); - if (!str_contains($php, 'TYPO3_CONTEXT')) { - $php = $this->getApplicationContext() . $php; - } - - $result = run( - $php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' install:fixfolderstructure' - ); - writeln($result); - } - - /** - * Clear cache via typo3 cache:flush - */ - public function remoteClearCache(): void - { - $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); - - $php = $this->getSudo() . parse('{{bin/php}} '); - if (!str_contains($php, 'TYPO3_CONTEXT')) { - $php = $this->getApplicationContext() . $php; - } - - $result = run($php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' cache:flush'); - writeln($result); - } - - /** - * Clear opcache via curl on {CI_HOST}/cache.php - */ - public function remoteClearOpcache(): void - { - $webDomain = rtrim($this->get('web_domain'), '/'); - $htaccess = $this->has('htaccess') ? '--user ' . $this->get('htaccess') : ''; - run('curl ' . $htaccess . ' -sk ' . $webDomain . '/cache.php'); - } - - public function echoReleaseNumber(): void - { - info('Folder of this release is: {{release_path}}'); + set($name, $value); } } diff --git a/Classes/Services/Environment.php b/Classes/Services/Environment.php deleted file mode 100644 index 375ed33..0000000 --- a/Classes/Services/Environment.php +++ /dev/null @@ -1,18 +0,0 @@ -getComposer()->getPackage()->getExtra(); - foreach (($extras['deployer'] ?? []) as $key => $value) { - putenv(strtoupper($key) . '=' . $value); - } - } -} diff --git a/Classes/Config/AbstractConfiguration.php b/Classes/Tasks/AbstractTasks.php similarity index 63% rename from Classes/Config/AbstractConfiguration.php rename to Classes/Tasks/AbstractTasks.php index 4ef2776..cfe5a48 100644 --- a/Classes/Config/AbstractConfiguration.php +++ b/Classes/Tasks/AbstractTasks.php @@ -1,21 +1,36 @@ setDefaultConfiguration(); + $this->initializeTasks(); + } + + protected function setDefaultConfiguration(): void + { + } + protected function initializeTasks(): void { array_walk($this->tasks, [$this, 'registerTask']); @@ -63,9 +78,9 @@ abstract class AbstractConfiguration return $summary; } - protected function get(string $name, $default = null) + protected function commandExist(string $command): bool { - return get($name, $default); + return testLocally("hash $command 2>/dev/null"); } protected function set(string $name, $value): void @@ -73,6 +88,11 @@ abstract class AbstractConfiguration set($name, $value); } + protected function get(string $name, $default = null) + { + return get($name, $default); + } + protected function has(string $name): bool { return has($name); @@ -82,4 +102,26 @@ abstract class AbstractConfiguration { return $this->has($name) && is_array($this->get($name)); } + + protected function getApplicationContext(): string + { + return $this->has('application_context') ? + 'TYPO3_CONTEXT="' . $this->get('application_context') . '" ' : + ''; + } + + protected function getSudo(): string + { + return $this->get('clear_use_sudo') ? 'sudo ' : ''; + } + + protected function getTYPO3Bin(string $path, string $testPath): string + { + if (test('[ -f ' . $testPath . '/releases/{{release_name}}/vendor/bin/typo3cms ]')) { + $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3cms'; + } else { + $bin = $path . '/releases/{{release_name}}/vendor/bin/typo3'; + } + return $bin; + } } diff --git a/Classes/Tasks/Deploy.php b/Classes/Tasks/Deploy.php new file mode 100644 index 0000000..82df28e --- /dev/null +++ b/Classes/Tasks/Deploy.php @@ -0,0 +1,61 @@ + [ + 'body' => [ + 'local:info', + 'local:setup', + 'remote:prepare', + 'rsync:warmup', + 'local:lock', + 'local:release', + 'local:update_code', + 'local:env', + 'local:shared', + 'local:writable', + 'local:write_release', + 'local:create_folder', + 'local:vendors', + ] + ], + + 'deploy:publish' => [ + 'body' => [ + 'local:clear_paths', + 'local:symlink', + 'local:cleanup', + 'rsync:remote', + 'remote:change_group', + 'remote:writable', + 'remote:db_compare', + 'remote:fix_folder_structure', + 'rsync:switch_current', + 'remote:clear_cache', + 'remote:clear_opcache', + 'local:echo_release_number', + 'local:unlock', + 'deploy:success' + ] + ], + ]; + + public function __construct() + { + parent::__construct(); + $this->registerAfterTask(); + } + + protected function registerAfterTask(): void + { + // [Optional] if deploy fails automatically unlock. + after('deploy:failed', 'local:unlock'); + } +} diff --git a/Classes/Tasks/Local.php b/Classes/Tasks/Local.php new file mode 100644 index 0000000..f31b5f8 --- /dev/null +++ b/Classes/Tasks/Local.php @@ -0,0 +1,486 @@ + ['body' => 'localInfo'], + 'local:setup' => ['body' => 'localSetup'], + 'local:lock' => ['body' => 'localLock'], + 'local:unlock' => ['body' => 'localUnlock'], + 'local:release' => ['body' => 'localRelease'], + 'local:update_code' => ['body' => 'localUpdateCode'], + 'local:env' => ['body' => 'localEnv'], + 'local:shared' => ['body' => 'localShared'], + 'local:writable' => ['body' => 'localWritable'], + 'local:write_release' => ['body' => 'localWriteRelease'], + 'local:create_folder' => ['body' => 'localCreateFolder'], + 'local:vendors' => ['body' => 'localVendors'], + 'local:clear_paths' => ['body' => 'localClearPaths'], + 'local:symlink' => ['body' => 'localSymlink'], + 'local:cleanup' => ['body' => 'localCleanup'], + 'local:echo_release_number' => ['body' => 'localEchoReleaseNumber'], + ]; + + /** + * Displays info about deployment + */ + public function localInfo(): void + { + info("deploying {{what}} to {{where}}"); + } + + /** + * Prepares host for deploy + */ + public function localSetup(): void + { + runLocally( + << {{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." + ); + } + } + + /** + * Unlocks deploy + */ + public function localUnlock(): void + { + // always success + runLocally("rm -f {{deploy_path}}/.dep/deploy.lock"); + } + + /** + * Prepares release + */ + public function localRelease(): 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 localUpdateCode(): void + { + $git = get('bin/git'); + $repository = get('repository'); + $target = get('target'); + + if (empty($repository)) { + throw new ConfigurationException("Missing 'repository' configuration."); + } + + $targetWithDir = $target; + if (!empty(get('sub_directory'))) { + $targetWithDir .= ':{{sub_directory}}'; + } + + $bare = parse('{{deploy_path}}/.dep/repo'); + $env = [ + 'GIT_TERMINAL_PROMPT' => '0', + 'GIT_SSH_COMMAND' => get('git_ssh_command'), + ]; + + start: + // Clone the repository to a bare repo. + runLocally("[ -d $bare ] || mkdir -p $bare"); + runLocally("[ -f $bare/HEAD ] || $git clone --mirror $repository $bare 2>&1", ['env' => $env]); + + // If remote url changed, drop `.dep/repo` and reinstall. + if (runLocally("cd $bare; $git config --get remote.origin.url") !== $repository) { + runLocally("rm -rf {{deploy_path}}$bare"); + goto start; + } + + runLocally("$git remote update 2>&1", ['env' => $env]); + + // Copy to release_path. + if (get('update_code_strategy') === 'archive') { + runLocally("cd $bare; $git archive $targetWithDir | tar -x -f - -C {{release_path}} 2>&1"); + } elseif (get('update_code_strategy') === 'clone') { + runLocally("cd {{release_path}}; $git clone -l $bare ."); + runLocally("cd {{release_path}}; $git remote set-url origin $repository", ['env' => $env]); + runLocally("cd {{release_path}}; $git checkout --force $target"); + } else { + throw new ConfigurationException(parse("Unknown `update_code_strategy` option: {{update_code_strategy}}.")); + } + + // Save git revision in REVISION file. + $rev = escapeshellarg(runLocally("cd $bare; $git rev-list $target -1")); + runLocally("echo $rev > {{release_path}}/REVISION"); + } + + /** + * Configure .env file + */ + public function localEnv(): 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 localShared(): 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 localWritable(): 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`.'); + } + + // Create directories if they don't exist + runLocally("cd {{release_or_current_path}}; mkdir -p $dirs"); + + if ($mode === 'chown') { + $httpUser = get('http_user'); + // Change owner. + // -L traverse every symbolic link to a directory encountered + runLocally("cd {{release_or_current_path}}; $sudo chown -L $recursive $httpUser $dirs"); + } elseif ($mode === 'chgrp') { + // Change group ownership. + // -L traverse every symbolic link to a directory encountered + runLocally("cd {{release_or_current_path}}; $sudo chgrp -L $recursive {{http_group}} $dirs"); + runLocally("cd {{release_or_current_path}}; $sudo chmod $recursive g+rwx $dirs"); + } elseif ($mode === 'chmod') { + runLocally("cd {{release_or_current_path}}; $sudo chmod $recursive {{writable_chmod_mode}} $dirs"); + } 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("cd {{release_or_current_path}}; $sudo chmod g+w $dirs"); + } 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("cd {{release_or_current_path}}; getfacl -p $dir | grep \"^user:$httpUser:.*w\" | wc -l"); + // Set ACL for directory if it has not been set before + if (!$hasfacl) { + runLocally("cd {{release_or_current_path}}; setfacl -L $recursive $setFaclUsers $dir"); + runLocally("cd {{release_or_current_path}}; setfacl -dL $recursive $setFaclUsers $dir"); + } + } + } else { + runLocally("cd {{release_or_current_path}}; $sudo setfacl -L $recursive $setFaclUsers $dirs"); + runLocally("cd {{release_or_current_path}}; $sudo setfacl -dL $recursive $setFaclUsers $dirs"); + } + } 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("cd {{release_or_current_path}}; 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'); + } 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 localWriteRelease(): void + { + runLocally('echo ' . getEnv('CI_COMMIT_REF_NAME') . ' > {{release_path}}/release'); + } + + /** + * Creating necessary folders + */ + public function localCreateFolder(): void + { + $sudo = $this->getSudo(); + + if (testLocally('[ ! -d {{release_path}}/var/cache ]')) { + runLocally('mkdir -p {{release_path}}/var/cache'); + } + runLocally($sudo . ' chmod -R 775 {{release_path}}/var/cache'); + + if (testLocally('[ ! -d {{release_path}}/var/log ]')) { + runLocally('mkdir -p {{release_path}}/var/log'); + } + runLocally($sudo . ' chmod -R 775 {{release_path}}/var/log'); + } + + /** + * Installing vendors + */ + public function localVendors(): void + { + if (!$this->commandExist('unzip')) { + info( + 'To speed up composer installation setup "unzip" command' . + ' with PHP zip extension https://goo.gl/sxzFcD' + ); + } + + // 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 localClearPaths(): 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 localSymlink(): 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 localCleanup(): 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 localEchoReleaseNumber(): void + { + info('Folder of this release is: {{release_path}}'); + } +} diff --git a/Classes/Tasks/Remote.php b/Classes/Tasks/Remote.php new file mode 100644 index 0000000..3304462 --- /dev/null +++ b/Classes/Tasks/Remote.php @@ -0,0 +1,165 @@ + ['body' => 'remotePrepare'], + 'remote:change_group' => ['body' => 'remoteChangeGroup'], + 'remote:writable' => ['body' => 'remoteWritable'], + 'remote:db_compare' => ['body' => 'remoteDbCompare'], + 'remote:fix_folder_structure' => ['body' => 'remoteFixFolderStructure'], + 'remote:clear_cache' => ['body' => 'remoteClearCache'], + 'remote:clear_opcache' => ['body' => 'remoteClearOpcache'], + ]; + + /** + * Prepare remote path exists + */ + public function remotePrepare(): void + { + if (!$this->hasArray('prepare_dirs')) { + return; + } + + $prepareDirs = $this->get('prepare_dirs'); + foreach ($prepareDirs as $dir) { + if (test('[ ! -e "{{remote_path}}/' . $dir . '" ]')) { + run($this->getSudo() . ' mkdir -p {{remote_path}}/' . $dir); + } + } + } + + /** + * Change usergroup for the hole project + */ + public function remoteChangeGroup(): void + { + if ($this->get('http_group')) { + $sudo = get('writable_use_sudo') ? 'sudo' : ''; + $runOpts = []; + if ($sudo) { + $runOpts['tty'] = $this->get('writable_tty', false); + } + + $path = parse('{{remote_path}}/releases/{{release_name}}/'); + run($sudo . ' chgrp -H -R ' . $this->get('http_group') . ' ' . $path, $runOpts); + run($sudo . ' chmod -R 2755 ' . $path, $runOpts); + } + } + + /** + * Set files and/or folders writable + */ + public function remoteWritable(): void + { + if ($this->hasArray('writable_dirs') || $this->hasArray('writable_files')) { + $sudo = get('writable_use_sudo') ? 'sudo' : ''; + $runOpts = []; + if ($sudo) { + $runOpts['tty'] = get('writable_tty', false); + } + $recursive = $this->get('writable_recursive') ? '-R' : ''; + $httpGroup = $this->get('http_group', false); + $path = parse('{{remote_path}}/releases/{{release_name}}/'); + + if ($this->hasArray('writable_dirs')) { + foreach ($this->get('writable_dirs') as $dir) { + if (test('[ ! -d "' . $path . $dir . '" ]')) { + run('mkdir -p ' . $path . $dir); + } + run($sudo . ' chmod -R 2775 ' . $path . $dir); + if ($httpGroup) { + run( + $sudo . ' chgrp -H ' . $recursive . ' ' . $httpGroup . ' ' . $path . $dir, + $runOpts + ); + } + } + } + + if ($this->hasArray('writable_files')) { + foreach ($this->get('writable_files') as $file) { + if (test('[ -e "' . $path . $file . '" ]')) { + run($sudo . ' chmod g+w ' . $path . $file); + if ($httpGroup) { + run($sudo . ' chgrp ' . $httpGroup . ' ' . $path . $file, $runOpts); + } + } + } + } + } + } + + /** + * Database migration + */ + public function remoteDbCompare(): void + { + $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); + + $php = $this->getSudo() . parse('{{bin/php}} '); + if (!str_contains($php, 'TYPO3_CONTEXT')) { + $php = $this->getApplicationContext() . $php; + } + + $result = run( + $php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' database:updateschema "*.add,*.change"' + ); + writeln($result); + } + + /** + * Fix folder structure + */ + public function remoteFixFolderStructure(): void + { + $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); + + $php = $this->getSudo() . parse('{{bin/php}} '); + if (!str_contains($php, 'TYPO3_CONTEXT')) { + $php = $this->getApplicationContext() . $php; + } + + $result = run( + $php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' install:fixfolderstructure' + ); + writeln($result); + } + + /** + * Clear cache via typo3 cache:flush + */ + public function remoteClearCache(): void + { + $path = parse('{{app_container_path}}') ?: parse('{{remote_path}}'); + + $php = $this->getSudo() . parse('{{bin/php}} '); + if (!str_contains($php, 'TYPO3_CONTEXT')) { + $php = $this->getApplicationContext() . $php; + } + + $result = run($php . $this->getTYPO3Bin($path, parse('{{remote_path}}')) . ' cache:flush'); + writeln($result); + } + + /** + * Clear opcache via curl on {CI_HOST}/cache.php + */ + public function remoteClearOpcache(): void + { + $webDomain = rtrim($this->get('web_domain'), '/'); + $htaccess = $this->has('htaccess') ? '--user ' . $this->get('htaccess') : ''; + run('curl ' . $htaccess . ' -sk ' . $webDomain . '/cache.php'); + } +} diff --git a/Classes/Config/Rsync.php b/Classes/Tasks/Rsync.php similarity index 97% rename from Classes/Config/Rsync.php rename to Classes/Tasks/Rsync.php index 2bff62b..7b9c74d 100644 --- a/Classes/Config/Rsync.php +++ b/Classes/Tasks/Rsync.php @@ -1,13 +1,15 @@ [ @@ -45,12 +47,6 @@ class Rsync extends AbstractConfiguration 'rsync:switch_current' => ['body' => 'switchCurrent'], ]; - public function __construct() - { - $this->setDefaultConfiguration(); - $this->initializeTasks(); - } - protected function setDefaultConfiguration(): void { $this->set('port', 22);