From 4a2b169ced525638f259a64886bfd9c6218c0374 Mon Sep 17 00:00:00 2001 From: Jelle Sebreghts Date: Mon, 7 Nov 2022 17:06:43 +0100 Subject: [PATCH] Refactor the whole package to use events Because of https://github.com/consolidation/annotated-command/pull/273, which technically speaking contains a BC break, it was made clear for us that we're not using Robo or the consolidation packages as they were intended. In an effort to keep this package as extendible as possible, and keeping the same philosophy as before, we switched to an event-based system. Basically every step in a command fires an event. The event listener returns a `HandlerWithPriority`, which it turn returns a `TaskInterface` to be executed for this step in the command. We made our own implementation of event priorities, since the default implementation used in Robo doesn't support them (https://github.com/consolidation/annotated-command/issues/244). I tried to document everything as well as possible in the README, but it might (probably will) need some lovin'. This package contains default event handlers with some sensible (to us) defaults. You can prevent these handlers from being executed by writig your own handler with a higher priority (lower number) which does its thing and then calls `$event->stopPropagation()`. --- README.md | 576 +++++++++++++++--- composer.json | 2 +- .../AppTaskFactoryAwareInterface.php | 10 - .../BackupTaskFactoryAwareInterface.php | 10 - .../BuildTaskFactoryAwareInterface.php | 10 - .../CacheTaskFactoryAwareInterface.php | 10 - .../DeployTaskFactoryAwareInterface.php | 10 - .../PropertiesHelperAwareInterface.php | 10 - .../RemoteHelperAwareInterface.php | 10 - src/DependencyInjection/ServiceProvider.php | 51 -- .../SyncTaskFactoryAwareInterface.php | 10 - .../Traits/AppTaskFactoryAware.php | 15 - .../Traits/BackupTaskFactoryAware.php | 15 - .../Traits/BuildTaskFactoryAware.php | 15 - .../Traits/CacheTaskFactoryAware.php | 15 - .../Traits/DeployTaskFactoryAware.php | 15 - .../Traits/PropertiesHelperAware.php | 15 - .../Traits/RemoteHelperAware.php | 15 - .../Traits/SyncTaskFactoryAware.php | 15 - src/EventHandler/AbstractBackupHandler.php | 34 ++ src/EventHandler/AbstractTaskEventHandler.php | 21 + .../DefaultHandler/BackupRemoteHandler.php | 56 ++ .../DefaultHandler/BuildTaskHandler.php | 24 + .../DefaultHandler/CleanDirsHandler.php | 36 ++ .../DefaultHandler/ClearCacheHandler.php | 19 + .../ClearRemoteOpcacheHandler.php | 36 ++ .../CompressOldReleaseHandler.php | 62 ++ .../CurrentProjectRootHandler.php | 58 ++ .../DefaultHandler/DownloadBackupHandler.php | 48 ++ .../DefaultHandler/InstallHandler.php | 19 + .../DefaultHandler/IsSiteInstalledHandler.php | 39 ++ .../DefaultHandler/LocalSettingsHandler.php | 36 ++ .../DefaultHandler/MirrorDirHandler.php | 45 ++ .../DefaultHandler/PostSymlinkHandler.php | 52 ++ .../PreLocalSyncFilesHandler.php | 34 ++ .../PreRestoreBackupRemoteHandler.php | 55 ++ .../DefaultHandler/PreSymlinkHandler.php | 79 +++ .../DefaultHandler/PushPackageHandler.php | 44 ++ .../DefaultHandler/RealpathHandler.php | 18 + .../DefaultHandler/RemoteSettingsHandler.php | 54 ++ .../RemoteSwitchPreviousHandler.php | 33 + .../DefaultHandler/RemoteSymlinkHandler.php | 45 ++ .../RemoveBackupRemoteHandler.php | 40 ++ .../RemoveFailedReleaseHandler.php | 32 + .../RemoveLocalBackupHandler.php | 32 + .../RestoreBackupDbLocalHandler.php | 41 ++ .../RestoreBackupFilesLocalHandler.php | 43 ++ .../RestoreBackupRemoteHandler.php | 66 ++ .../RsyncFilesBetweenHostsHandler.php | 278 +++++++++ .../RsyncFilesToLocalHandler.php | 43 ++ .../DefaultHandler/SettingsHandler.php | 110 ++++ .../DefaultHandler/SwitchPreviousHandler.php | 23 + .../DefaultHandler/TimeoutSettingHandler.php | 74 +++ .../DefaultHandler/UpdateHandler.php | 19 + .../DefaultHandler/UploadBackupHandler.php | 48 ++ src/EventHandler/EventHandlerWithPriority.php | 27 + .../Commands/DigipolisHelpersCommands.php | 296 --------- .../DigipolisHelpersDefaultHooksCommands.php | 361 +++++++++++ .../DigipolisHelpersDeployCommand.php | 45 ++ .../DigipolisHelpersMirrorDirCommand.php | 35 ++ .../DigipolisHelpersRealPathCommand.php | 33 + .../DigipolisHelpersSwitchPreviousCommand.php | 32 + .../Commands/DigipolisHelpersSyncCommand.php | 67 ++ .../DigipolisHelpersSyncLocalCommand.php | 129 ++++ src/Robo/Plugin/Tasks/Remote.php | 1 + src/RoboFile.php | 5 + src/Traits/CommandWithBackups.php | 152 +++++ .../DigipolisHelpersCommandUtilities.php | 224 +++++++ ...DigipolisHelpersDeployCommandUtilities.php | 371 +++++++++++ .../DigipolisHelpersSyncCommandUtilities.php | 172 ++++++ src/Traits/EventDispatcher.php | 113 ++++ src/Traits/RemoteFilesBackupTrait.php | 4 +- src/Util/AddToContainerInterface.php | 7 + src/Util/RemoteConfig.php | 99 +++ src/Util/RemoteHelper.php | 333 ---------- src/Util/TaskFactory/AbstractApp.php | 97 --- src/Util/TaskFactory/Backup.php | 334 ---------- src/Util/TaskFactory/BackupConfigTrait.php | 26 - src/Util/TaskFactory/Build.php | 58 -- src/Util/TaskFactory/Cache.php | 83 --- src/Util/TaskFactory/Deploy.php | 539 ---------------- src/Util/TaskFactory/Sync.php | 412 ------------- src/Util/TimeHelper.php | 47 ++ src/default.properties.yml | 21 + 84 files changed, 4220 insertions(+), 2528 deletions(-) delete mode 100644 src/DependencyInjection/AppTaskFactoryAwareInterface.php delete mode 100644 src/DependencyInjection/BackupTaskFactoryAwareInterface.php delete mode 100644 src/DependencyInjection/BuildTaskFactoryAwareInterface.php delete mode 100644 src/DependencyInjection/CacheTaskFactoryAwareInterface.php delete mode 100644 src/DependencyInjection/DeployTaskFactoryAwareInterface.php delete mode 100644 src/DependencyInjection/PropertiesHelperAwareInterface.php delete mode 100644 src/DependencyInjection/RemoteHelperAwareInterface.php delete mode 100644 src/DependencyInjection/ServiceProvider.php delete mode 100644 src/DependencyInjection/SyncTaskFactoryAwareInterface.php delete mode 100644 src/DependencyInjection/Traits/AppTaskFactoryAware.php delete mode 100644 src/DependencyInjection/Traits/BackupTaskFactoryAware.php delete mode 100644 src/DependencyInjection/Traits/BuildTaskFactoryAware.php delete mode 100644 src/DependencyInjection/Traits/CacheTaskFactoryAware.php delete mode 100644 src/DependencyInjection/Traits/DeployTaskFactoryAware.php delete mode 100644 src/DependencyInjection/Traits/PropertiesHelperAware.php delete mode 100644 src/DependencyInjection/Traits/RemoteHelperAware.php delete mode 100644 src/DependencyInjection/Traits/SyncTaskFactoryAware.php create mode 100644 src/EventHandler/AbstractBackupHandler.php create mode 100644 src/EventHandler/AbstractTaskEventHandler.php create mode 100644 src/EventHandler/DefaultHandler/BackupRemoteHandler.php create mode 100644 src/EventHandler/DefaultHandler/BuildTaskHandler.php create mode 100644 src/EventHandler/DefaultHandler/CleanDirsHandler.php create mode 100644 src/EventHandler/DefaultHandler/ClearCacheHandler.php create mode 100644 src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php create mode 100644 src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php create mode 100644 src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php create mode 100644 src/EventHandler/DefaultHandler/DownloadBackupHandler.php create mode 100644 src/EventHandler/DefaultHandler/InstallHandler.php create mode 100644 src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php create mode 100644 src/EventHandler/DefaultHandler/LocalSettingsHandler.php create mode 100644 src/EventHandler/DefaultHandler/MirrorDirHandler.php create mode 100644 src/EventHandler/DefaultHandler/PostSymlinkHandler.php create mode 100644 src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php create mode 100644 src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php create mode 100644 src/EventHandler/DefaultHandler/PreSymlinkHandler.php create mode 100644 src/EventHandler/DefaultHandler/PushPackageHandler.php create mode 100644 src/EventHandler/DefaultHandler/RealpathHandler.php create mode 100644 src/EventHandler/DefaultHandler/RemoteSettingsHandler.php create mode 100644 src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php create mode 100644 src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php create mode 100644 src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php create mode 100644 src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php create mode 100644 src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php create mode 100644 src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php create mode 100644 src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php create mode 100644 src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php create mode 100644 src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php create mode 100644 src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php create mode 100644 src/EventHandler/DefaultHandler/SettingsHandler.php create mode 100644 src/EventHandler/DefaultHandler/SwitchPreviousHandler.php create mode 100644 src/EventHandler/DefaultHandler/TimeoutSettingHandler.php create mode 100644 src/EventHandler/DefaultHandler/UpdateHandler.php create mode 100644 src/EventHandler/DefaultHandler/UploadBackupHandler.php create mode 100644 src/EventHandler/EventHandlerWithPriority.php delete mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersCommands.php create mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands.php create mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersDeployCommand.php create mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersMirrorDirCommand.php create mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersRealPathCommand.php create mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersSwitchPreviousCommand.php create mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php create mode 100644 src/Robo/Plugin/Commands/DigipolisHelpersSyncLocalCommand.php create mode 100644 src/RoboFile.php create mode 100644 src/Traits/CommandWithBackups.php create mode 100644 src/Traits/DigipolisHelpersCommandUtilities.php create mode 100644 src/Traits/DigipolisHelpersDeployCommandUtilities.php create mode 100644 src/Traits/DigipolisHelpersSyncCommandUtilities.php create mode 100644 src/Traits/EventDispatcher.php create mode 100644 src/Util/AddToContainerInterface.php create mode 100644 src/Util/RemoteConfig.php delete mode 100644 src/Util/RemoteHelper.php delete mode 100644 src/Util/TaskFactory/AbstractApp.php delete mode 100644 src/Util/TaskFactory/Backup.php delete mode 100644 src/Util/TaskFactory/BackupConfigTrait.php delete mode 100644 src/Util/TaskFactory/Build.php delete mode 100644 src/Util/TaskFactory/Cache.php delete mode 100644 src/Util/TaskFactory/Deploy.php delete mode 100644 src/Util/TaskFactory/Sync.php create mode 100644 src/Util/TimeHelper.php create mode 100644 src/default.properties.yml diff --git a/README.md b/README.md index 37194f3..afaf6e0 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,23 @@ [![Build Status](https://travis-ci.org/digipolisgent/robo-digipolis-helpers.svg?branch=develop)](https://travis-ci.org/digipolisgent/robo-digipolis-helpers) [![Maintainability](https://api.codeclimate.com/v1/badges/1c4c5693cb7945f5e5e9/maintainability)](https://codeclimate.com/github/digipolisgent/robo-digipolis-helpers/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/1c4c5693cb7945f5e5e9/test_coverage)](https://codeclimate.com/github/digipolisgent/robo-digipolis-helpers/test_coverage) -[![PHP 7 ready](https://php7ready.timesplinter.ch/digipolisgent/robo-digipolis-helpers/develop/badge.svg)](https://travis-ci.org/digipolisgent/robo-digipolis-helpers) -Used by digipolis, abstract robo file to help with the deploy flow. +Used by digipolis, generic commands/skeleton do execute deploys and syncs between environments. -By default, we assume a [capistrano-like directory structure](http://capistranorb.com/documentation/getting-started/structure/): +## Getting started + +We make a couple of assumptions, most of which can be overwritten. See +[default.properties.yml](src/default.properties.yml) for all default values, and +[the properties.yml documentation](#propertiesyml) for all available +configuration options. + +By default, we assume a [capistrano-like directory structure](http://capistranorb.com/documentation/getting-started/structure/) +on your servers: ``` -├── current -> releases/20150120114500/ -├── releases +├── ~/apps/[app]/current -> ~/apps/[app]/releases/20150120114500/ +├── ~/apps/[app]/releases │ ├── 20150080072500 │ ├── 20150090083000 │ ├── 20150100093500 @@ -25,92 +32,25 @@ By default, we assume a [capistrano-like directory structure](http://capistranor │ └── 20150120114500 ``` -## Example implementation - -### RoboFile.php - -```php -collectionBuilder(); - $collection->addTask($this->taskExec('phpcs --standard=PSR2 ./src')); - return $collection; - } - - /** - * Detects whether this site is installed or not. This method is used to - * determine whether we should run `updateTask` (if this returns `true`) or - * `installTask` (if this returns `false`). - */ - protected function isSiteInstalled($worker, AbstractAuth $auth, $remote) - { - $currentProjectRoot = $this->getCurrentProjectRoot($worker, $auth, $remote); - $migrateStatus = ''; - return $this->taskSsh($worker, $auth) - ->remoteDirectory($currentProjectRoot, true) - ->exec('ls -al | grep index.php') - ->run() - ->wasSuccessful(); - } - - protected function updateTask($worker, AbstractAuth $auth, $remote) - { - $currentProjectRoot = $remote['rootdir']; - return $this->taskSsh($server, $auth) - ->remoteDirectory($currentProjectRoot, true) - ->exec('./update.sh'); - } - - protected function installTask( - $worker, - AbstractAuth $auth, - $remote, - $extra = [], - $force = false - ) { - $currentProjectRoot = $remote['rootdir']; - return $this->taskSsh($server, $auth) - ->remoteDirectory($currentProjectRoot, true) - ->exec('./install.sh'); - } - - /** - * Build a my site and push it to the server(s). - * - * @param array $arguments - * Variable amount of arguments. The last argument is the path to the - * the private key file (ssh), the penultimate is the ssh user. All - * arguments before that are server IP's to deploy to. - * @param array $opts - * The options for this command. - * - * @option option1 Description of the first option. - * @option option2 Description of the second option. - * - * @usage --option1=first --option2=2 192.168.1.2 sshuser /home/myuser/.ssh/id_rsa - */ - public function myDeployCommand( - array $arguments, - $opts = ['option1' => 'one', 'option2' => 'two'] - ) { - return $this->deployTask($arguments, $opts); - } -} -``` - -If you place this in `RoboFile.php` in your project root, you'll be able to run -`vendor/bin/robo my:deploy-command --option1=1 --option2=2 192.168.1.2 sshuser /home/myuser/.ssh/id_rsa` -to release your website. The script will automatically detect whether it should -update your site or do a fresh install, based on your implementation of -`isSiteInstalled`. Note that this command can only run after the `composer install` -command completed successfully (without any errors). +This package provides a couple of commands. You can use `vendor/bin/robo list` +and `vendor/bin/robo help [command]` to find out what they do. Most importantly +these commands follow a "skeleton", in which each step of the command fires an +event, and the event listeners return an +[EventHandlerWithPriority](src/EventHandler/EventHandlerWithPriority). The +default event listeners provided by this package are in the +[DigipolisHelpersDefaultHooksCommands](src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands) +class. Each method of that class is an event listener, and returns an event +handler. The default handlers provided by this package can be found in +[src/EventHandler/DefaultHandler](src/EventHandler/DefaultHandler). If you want +to overwrite or alter the behavior of a certain step in the command, all you +have to do is +[create an event listener by using the on-event hook](https://github.com/consolidation/annotated-command#on-event-hook) +for the right event, and let it return your custom handler. Handlers are +executed in order of priority (lower numbers executed first), the priority of +default handlers is 999. If your handler calls `$event->stopPropagation()` in +its `handle` method, handlers that come after it, won't get executed. For +further information, see the +[list of available events](#list-of-available-events); ### properties.yml @@ -122,7 +62,7 @@ Below is an example of some sensible defaults: ```YAML remote: # The application directory where your capistrano folder structure resides. - appdir: '/home/[user]' + appdir: '/home/[user]/apps/[app]' # The releases directory where to deploy new releases to. releasesdir: '${remote.appdir}/releases' # The root directory of a new release. @@ -215,7 +155,7 @@ timeouts: restore_db_backup: 60 # Before a files backup is restored, the current files are removed. This is # the timeout for removing those files. - pre_restore_remove_files: 300 + pre_restore: 300 # See ${remote.cleandir_limit}. This is the timeout for that operation. clean_dir: 30 ``` @@ -225,7 +165,451 @@ following notation: `${path.to.property}`. There are also other tokens available: ``` -[user] The ssh user we used to connect to the server. -[time] New releases are put in a folder with the current timestamp as folder - name. This is that timestamp. +[user] The ssh user we used to connect to the server. +[private-key] The path to the private key that was used to connect to the + server. +[time] New releases are put in a folder with the current timestamp as + folder name. This is that timestamp. +[app] The name of the app that is being deployed. ``` + +### List of available events + +Event arguments can be retrieved with `$event->getArgument($argumentName);` + +#### digipolis:backup-remote + +The handler for this event should return a task that creates a backup on a +host, based on options that are passed. + +*Default handler*: [BackupRemoteHandler](src/EventHandler/DefaultHandler/BackupRemoteHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app of which we're going to create a backup. + - options: Options for the backup. An array with keys: + - files (bool): Whether or not to create a backup of the files. + - data (bool): Whether or not to create a backup of the database. + - fileBackupConfig: Configuration for the file backup. An array with keys: + - exclude_from_backup: Files and/or directories to exclude from the backup. + - file_backup_subdirs: The subdirectories of the files directory that need + to be backed up. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - backup_files: Timeout in seconds for the files backup. + - backup_database: Timeout in second for the database backup. + +### digipolis:build-task + +The handler for this event should return a task that creates a release archive +of the current codebase to upload to an environment. + +*Default handler*: [BuildTaskHandler](src/EventHandler/DefaultHandler/BuildTaskHandler.php)
+*Event arguments*: + - archiveName: The name of the archive that should be created. + +### digipolis:clean-dirs + +The handler for this event should return a task that cleans the releases +directory by removing the older releases. + +*Default handler*: [CleanDirsHandler](src/EventHandler/DefaultHandler/CleanDirsHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app of which we're going to clean the releases + directory. + +### digipolis:clear-cache + +The handler for this event should return a task that clears the cache on the +remote host. + +*Default handler*: [ClearCacheHandler](src/EventHandler/DefaultHandler/ClearCacheHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app of which we're going to clear the cache. + +### digipolis:clear-remote-opcache + +The handler for this event should return a task that clears the opcache on the +remote host. + +*Default handler*: [ClearRemoteOpcacheHandler](src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app of which we're going to clear the opcache. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - clear_op_cache: Timeout in seconds for clearing the opcache. + +### digipolis:compress-old-release + +The handler for this event should return a task that compresses old releases on +the host for the given app. + +*Default handler*: [CompressOldReleaseHandler](src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app of which we're going to compress the old + releases. + - releaseToCompress: The path to the release directory that should be + compressed. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - compress_old_release: Timeout in seconds for compressing the release. + +### digipolis:current-project-root + +The handler for this event should return the path to the current project root +for the given app on the given host. This means the actual path, not a task that +will return it when executed. + +*Default handler*: [CurrentProjectRootHandler](src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php)
+*Event arguments*: + - host: The host on which to get the project root. + - user: The SSH user to connect to the host. + - privateKeyFile: The path to the private key to use to connect to the host. + - remoteSettings: The remote settings for the given host and app as parsed + from `properties.yml`. + +### digipolis:download-backup + +The handler for this event should return a task that downloads a backup of an +app from a host. + +*Default handler*: [DownloadBackupHandler](src/EventHandler/DefaultHandler/DownloadBackupHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app of which we're going to download a backup. + - options: Options for the backup. An array with keys: + - files (bool): Whether or not a backup of the files was created. + - data (bool): Whether or not a backup of the database was created. + +### digipolis:install + +The handler for this event should return a task that executes the install script +on the host. + +*Default handler*: [InstallHandler](src/EventHandler/DefaultHandler/InstallHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app we're going to install. + - options: Options passed from the command to the install task. + - force: Boolean indicating whether or not to force the install, even if there + already is an installation. + +### digipolis:is-site-installed + +The handler for this event should return a boolean indicating whether or not +there already is an active installation of the app on the host. This means the +actual boolean, not a task that will return it when executed. This helps us to +determine whether the install or the update script should be ran when deploying +the app. + +*Default handler*: [IsSiteInstalledHandler](src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app we're checking. + +### digipolis:get-local-settings + +The handler for this event should return the settings for the local installation +of the app as parsed from `properties.yml`. + +*Default handler*: [LocalSettingsHandler](src/EventHandler/DefaultHandler/LocalSettingsHandler.php)
+*Event arguments*: + - app: The name of the app. + - timestamp: The current timestamp (sometimes used as token in paths). + +#### digipolis:mirror-dir + +The handler for this event should return a task that mirrors everything (files, +symlink, subdirectories, ...) from one directory to another. + +*Default Handler*: [MirrorDirHandler](src/EventHandler/DefaultHandler/MirrorDirHandler.php)
+*Event arguments*: + - dir: The directory to mirror. + - destination: The destination path to mirror the directory to. + +### digipolis:post-symlink + +The handler for this event should return a task that will be executed after +creating the symlinks (as parsed from `properties.yml`) on the remote host. + +*Default Handler*: [PostSymlinkHandler](src/EventHandler/DefaultHandler/PostSymlinkHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - post_symlink: Timeout in seconds for the post symlink tasks. + +### digipolis:pre-local-sync-files + +The handler for this event should return a task that should be executed before +syncing files from a remote installation to your local installation. + +*Default Handler*: [PreLocalSyncFilesHandler](src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - localSettings: the settings for the local installation of the app as parsed + from `properties.yml`. + +### digipolis:pre-restore-backup-remote + +The handler for this event should return a task that should be executed before +restoring a backup on a host. + +*Default Handler*: [PreRestoreBackupRemoteHandler](src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - fileBackupConfig: Configuration for the file backup. An array with keys: + - exclude_from_backup: Files and/or directories to exclude from the backup. + - file_backup_subdirs: The subdirectories of the files directory that need + to be backed up. + - options: Options for the backup. An array with keys: + - files (bool): Whether or not a backup of the files was created. + - data (bool): Whether or not a backup of the database was created. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - pre_restore: Timeout in seconds for the pre restore task. + +### digipolis:pre-symlink + +The handler for this event should return a task that should be executed before +the symlinks on the remote host are created. + +*Default Handler*: [PreSymlinkHandler](src/EventHandler/DefaultHandler/PreSymlinkHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - pre_symlink: Timeout in seconds for the pre symlink task. + +### digipolis:push-package + +The handler for this event should return a task that pushes a release archive to +a host. + +*Default Handler*: [PushPackageHandler](src/EventHandler/DefaultHandler/PushPackageHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - archiveName: The name of the archive that should be pushed. + +### digipolis:realpath + +The handler for this event should return the `realpath` of the given path. This +means the actual path, not a task that will return it when executed. The default +handler supports replacing `~` (tilde) with the user's homedir. + +*Default handler*: [RealpathHandler](src/EventHandler/DefaultHandler/RealpathHandler.php)
+*Event arguments*: + - path: The path to get the real path for. + +### digipolis:get-remote-settings + +The handler for this event should return the settings for the remote +installation of the app as parsed from `properties.yml`. This means the actual +settings, not a task that will return it when executed. + +*Default handler*: [RemoteSettingsHandler](src/EventHandler/DefaultHandler/RemoteSettingsHandler.php)
+*Event arguments*: + - servers: An array of servers (can be one, or multiple for loadbalanced + setups) where the app resides. + - user: The SSH user to connect to the servers. + - privateKeyFile: The path to the private key to use to connect to the + servers. + - app: The name of the app. + - timestamp: The current timestamp (sometimes used as token in paths). + +### digipolis:remote-switch-previous + +The handler for this event should return a task that will switch the `current` +symlink to the previous release (mostly used on rollback of a failed release). + +*Default Handler*: [RemoteSwitchPreviousHandler](src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + +### digipolis:remote-symlink + +The handler for this event should return a task that will create the symlinks as +defined in `properties.yml`. + +*Default Handler*: [RemoteSymlinkPreviousHandler](src/EventHandler/DefaultHandler/RemoteSymlinkPreviousHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + +### digipolis:remove-backup-remote + +The handler for this event should return a task that removes a backup from the +host. + +*Default Handler*: [RemoveBackupRemoteHandler](src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - options: Options for the backup. An array with keys: + - files (bool): Whether or not a backup of the files was created. + - data (bool): Whether or not a backup of the database was created. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - remove_backup: Timeout in seconds for the pre symlink task. + +### digipolis:remove-failed-release + +The handler for this event should return a task that removes a failed release +from the host. + +*Default Handler*: [RemoveFailedReleaseHandler](src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - releaseDir: The release directory to remove. + +### digipolis:remove-local-backup + +The handler for this event should return a task that removes a backup from your +local machine. + +*Default Handler*: [RemoveLocalBackupHandler](src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - options: Options for the backup. An array with keys: + - files (bool): Whether or not a backup of the files was created. + - data (bool): Whether or not a backup of the database was created. + - fileBackupConfig: Configuration for the file backup. An array with keys: + - exclude_from_backup: Files and/or directories to exclude from the backup. + - file_backup_subdirs: The subdirectories of the files directory that need + to be backed up. + +### digipolis:restore-backup-db-local + +The handler for this event should return a task that restores a database backup +on your local machine. + +*Default Handler*: [RestoreBackupDbLocalHandler](src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - localSettings: the settings for the local installation of the app as parsed + from `properties.yml`. + +### digipolis:restore-backup-files-local + +The handler for this event should return a task that restores a files backup on +your local machine. + +*Default Handler*: [RestoreBackupFilesLocalHandler](src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - localSettings: the settings for the local installation of the app as parsed + from `properties.yml`. + +### digipolis:restore-backup-remote + +The handler for this event should return a task that restores a backup on a +host. + +*Default Handler*: [RestoreBackupRemoteHandler](src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - options: Options for the backup. An array with keys: + - files (bool): Whether or not to create a backup of the files. + - data (bool): Whether or not to create a backup of the database. + - fileBackupConfig: Configuration for the file backup. An array with keys: + - exclude_from_backup: Files and/or directories to exclude from the backup. + - file_backup_subdirs: The subdirectories of the files directory that need + to be backed up. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - restore_files_backup: Timeout in seconds for the files backup. + - restore_db_backup: Timeout in second for the database backup. + +### digipolis:rsync-files-between-hosts + +The handler for this event should return a task that rsyncs files between two +hosts. + +*Default Handler*: [RsyncFilesBetweenHostsHandler](src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php)
+*Event arguments*: + - sourceRemoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object + with data relevant to the source host and app. + - destinationRemoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) + object with data relevant to the destination host and app. + - fileBackupConfig: Configuration for the file backup. An array with keys: + - exclude_from_backup: Files and/or directories to exclude from the backup. + - file_backup_subdirs: The subdirectories of the files directory that need + to be backed up. + - timeouts: SSH timeouts for relevant tasks. An array with keys: + - synctask_rsync: Timeout in seconds for the rsync. + +### digipolis:rsync-files-to-local + +The handler for this event should return a task that rsyncs files to your local +machine. + +*Default Handler*: [RsyncFilesToLocalHandler](src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app. + - localSettings: the settings for the local installation of the app as parsed + from `properties.yml`. + - directory: The subdirectory under the `$remoteSettings['filesdir']` that + should be synced. + - - fileBackupConfig: Configuration for the file backup. An array with keys: + - exclude_from_backup: Files and/or directories to exclude from the backup. + - file_backup_subdirs: The subdirectories of the files directory that need + to be backed up. + +### digipolis:switch-previous + +The handler for this event should return a task that will switch the `current` +symlink to the previous release (mostly used on rollback of a failed release). +The difference with the (digipolis:remote-switch-previous)[#digipolis-remote-switch-previous] +event is that this will be executed directly on the host, and thus doesn't need +an ssh connection, while the (digipolis:remote-switch-previous)[#digipolis-remote-switch-previous] +will be executed from your deployment server, or your local machine, and thus +will need an ssh connection to the host. + +*Default Handler*: [SwitchPreviousHandler](src/EventHandler/DefaultHandler/SwitchPreviousHandler.php)
+*Event arguments*: + - releasesDir: The directory containing all your releases. + - currentSymlink: The path to your `current` symlink. + +### digipolis:timeout-setting + +The handler for this event should return the the timeout setting of the given +type in seconds. This means the actual setting, not a task that will return it +when executed. + +*Default handler*: [TimeoutSettingHandler](src/EventHandler/DefaultHandler/TimeoutSettingHandler.php)
+*Event arguments*: + - type: The type of timeout setting to get. See timeout event arguments for + the other events. + +### digipolis:update + +The handler for this event should return a task that executes the update script +on the host. + +*Default handler*: [UpdateHandler](src/EventHandler/DefaultHandler/UpdateHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app we're going to update. + - options: Options passed from the command to the update task. + - force: Boolean indicating whether or not to force the install, even if there + already is an installation. + +### digipolis:upload-backup + +The handler for this event should return a task that uploads a backup of an +app to a host. + +*Default handler*: [UploadBackupHandler](src/EventHandler/DefaultHandler/UploadBackupHandler.php)
+*Event arguments*: + - remoteConfig: The [RemoteConfig](src/Util/RemoteConfig.php) object with data + relevant to the host and app of which we're going to download a backup. + - options: Options for the backup. An array with keys: + - files (bool): Whether or not a backup of the files was created. + - data (bool): Whether or not a backup of the database was created. diff --git a/composer.json b/composer.json index 77ef0c9..bda9f47 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "digipolisgent/robo-digipolis-general": "^2.0", "digipolisgent/command-builder": "^1.2.1", "roave/better-reflection": "^5.0", - "consolidation/annotated-command": "^4, <=4.5.6" + "symfony/event-dispatcher": "^6.1" }, "require-dev": { "phpunit/phpunit": "^9.5.20" diff --git a/src/DependencyInjection/AppTaskFactoryAwareInterface.php b/src/DependencyInjection/AppTaskFactoryAwareInterface.php deleted file mode 100644 index 6f4de73..0000000 --- a/src/DependencyInjection/AppTaskFactoryAwareInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -getContainer(); - $container->addShared('digipolis.time', time()); - $container->addShared(PropertiesHelper::class, [PropertiesHelper::class, 'create']) - ->addArgument($container); - $container->addShared(RemoteHelper::class, [RemoteHelper::class, 'create']) - ->addArgument($container); - $container->addShared(Backup::class, [Backup::class, 'create']) - ->addArgument($container); - $container->addShared(Build::class, [Build::class, 'create']) - ->addArgument($container); - $container->addShared(Cache::class, [Cache::class, 'create']) - ->addArgument($container); - $container->addShared(Deploy::class, [Deploy::class, 'create']) - ->addArgument($container); - $container->addShared(Sync::class, [Sync::class, 'create']) - ->addArgument($container); - } -} diff --git a/src/DependencyInjection/SyncTaskFactoryAwareInterface.php b/src/DependencyInjection/SyncTaskFactoryAwareInterface.php deleted file mode 100644 index 248ef61..0000000 --- a/src/DependencyInjection/SyncTaskFactoryAwareInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -appTaskFactory = $appTaskFactory; - } -} diff --git a/src/DependencyInjection/Traits/BackupTaskFactoryAware.php b/src/DependencyInjection/Traits/BackupTaskFactoryAware.php deleted file mode 100644 index c62f6df..0000000 --- a/src/DependencyInjection/Traits/BackupTaskFactoryAware.php +++ /dev/null @@ -1,15 +0,0 @@ -backupTaskFactory = $backupTaskFactory; - } -} diff --git a/src/DependencyInjection/Traits/BuildTaskFactoryAware.php b/src/DependencyInjection/Traits/BuildTaskFactoryAware.php deleted file mode 100644 index 85adcf5..0000000 --- a/src/DependencyInjection/Traits/BuildTaskFactoryAware.php +++ /dev/null @@ -1,15 +0,0 @@ -buildTaskFactory = $buildTaskFactory; - } -} diff --git a/src/DependencyInjection/Traits/CacheTaskFactoryAware.php b/src/DependencyInjection/Traits/CacheTaskFactoryAware.php deleted file mode 100644 index f8f0897..0000000 --- a/src/DependencyInjection/Traits/CacheTaskFactoryAware.php +++ /dev/null @@ -1,15 +0,0 @@ -cacheTaskFactory = $cacheTaskFactory; - } -} diff --git a/src/DependencyInjection/Traits/DeployTaskFactoryAware.php b/src/DependencyInjection/Traits/DeployTaskFactoryAware.php deleted file mode 100644 index 422375c..0000000 --- a/src/DependencyInjection/Traits/DeployTaskFactoryAware.php +++ /dev/null @@ -1,15 +0,0 @@ -deployTaskFactory = $deployTaskFactory; - } -} diff --git a/src/DependencyInjection/Traits/PropertiesHelperAware.php b/src/DependencyInjection/Traits/PropertiesHelperAware.php deleted file mode 100644 index 1e5c88f..0000000 --- a/src/DependencyInjection/Traits/PropertiesHelperAware.php +++ /dev/null @@ -1,15 +0,0 @@ -propertiesHelper = $propertiesHelper; - } -} diff --git a/src/DependencyInjection/Traits/RemoteHelperAware.php b/src/DependencyInjection/Traits/RemoteHelperAware.php deleted file mode 100644 index e18d71d..0000000 --- a/src/DependencyInjection/Traits/RemoteHelperAware.php +++ /dev/null @@ -1,15 +0,0 @@ -remoteHelper = $remoteHelper; - } -} diff --git a/src/DependencyInjection/Traits/SyncTaskFactoryAware.php b/src/DependencyInjection/Traits/SyncTaskFactoryAware.php deleted file mode 100644 index f0347ed..0000000 --- a/src/DependencyInjection/Traits/SyncTaskFactoryAware.php +++ /dev/null @@ -1,15 +0,0 @@ -syncTaskFactory = $syncTaskFactory; - } -} diff --git a/src/EventHandler/AbstractBackupHandler.php b/src/EventHandler/AbstractBackupHandler.php new file mode 100644 index 0000000..7b32f48 --- /dev/null +++ b/src/EventHandler/AbstractBackupHandler.php @@ -0,0 +1,34 @@ +getTime(); + } + return $timestamp . '_' . date('Y_m_d_H_i_s', $timestamp) . $extension; + } +} diff --git a/src/EventHandler/AbstractTaskEventHandler.php b/src/EventHandler/AbstractTaskEventHandler.php new file mode 100644 index 0000000..48a8952 --- /dev/null +++ b/src/EventHandler/AbstractTaskEventHandler.php @@ -0,0 +1,21 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $options = $event->getArgument('options'); + $fileBackupConfig = $event->getArgument('fileBackupConfig'); + $timeouts = $event->getArgument('timeouts'); + + if (!$options['files'] && !$options['data']) { + $options['files'] = true; + $options['data'] = true; + } + + $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time']; + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $collection = $this->collectionBuilder(); + + if ($options['files']) { + $collection + ->taskRemoteFilesBackup($remoteConfig->getHost(), $auth, $backupDir, $remoteSettings['filesdir']) + ->backupFile($this->backupFileName('.tar.gz')) + ->excludeFromBackup($fileBackupConfig['exclude_from_backup']) + ->backupSubDirs($fileBackupConfig['file_backup_subdirs']) + ->timeout($timeouts['backup_files']); + } + + if ($options['data']) { + $collection + ->taskRemoteDatabaseBackup($remoteConfig->getHost(), $auth, $backupDir, $remoteConfig->getCurrentProjectRoot()) + ->backupFile($this->backupFileName('.sql')) + ->timeout($timeouts['backup_database']); + } + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/BuildTaskHandler.php b/src/EventHandler/DefaultHandler/BuildTaskHandler.php new file mode 100644 index 0000000..baae4d5 --- /dev/null +++ b/src/EventHandler/DefaultHandler/BuildTaskHandler.php @@ -0,0 +1,24 @@ +hasArgument('archiveName') ? $event->getArgument('archiveName') : null; + $archive = is_null($archiveName) ? TimeHelper::getInstance()->getTime() . '.tar.gz' : $archiveName; + + return $this->taskPackageProject($archive); + } +} diff --git a/src/EventHandler/DefaultHandler/CleanDirsHandler.php b/src/EventHandler/DefaultHandler/CleanDirsHandler.php new file mode 100644 index 0000000..8e828e1 --- /dev/null +++ b/src/EventHandler/DefaultHandler/CleanDirsHandler.php @@ -0,0 +1,36 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + + $cleandirLimit = isset($remoteSettings['cleandir_limit']) ? max(1, $remoteSettings['cleandir_limit']) : ''; + $collection = $this->collectionBuilder(); + $collection->taskRemoteCleanDirs($remoteConfig->getHost(), $auth, $remoteSettings['rootdir'], $remoteSettings['releasesdir'], ($cleandirLimit ? ($cleandirLimit + 1) : false)); + + if ($remoteSettings['createbackup']) { + $collection->taskRemoteCleanDirs($remoteConfig->getHost(), $auth, $remoteSettings['rootdir'], $remoteSettings['backupsdir'], ($cleandirLimit ? ($cleandirLimit) : false)); + } + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/ClearCacheHandler.php b/src/EventHandler/DefaultHandler/ClearCacheHandler.php new file mode 100644 index 0000000..4892c9e --- /dev/null +++ b/src/EventHandler/DefaultHandler/ClearCacheHandler.php @@ -0,0 +1,19 @@ +collectionBuilder(); + } +} diff --git a/src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php b/src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php new file mode 100644 index 0000000..6c4766f --- /dev/null +++ b/src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php @@ -0,0 +1,36 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $timeouts = $event->getArgument('timeouts'); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + + $clearOpcache = CommandBuilder::create('vendor/bin/robo digipolis:clear-op-cache')->addArgument($remoteSettings['opcache']['env']); + if (isset($remoteSettings['opcache']['host'])) { + $clearOpcache->addOption('host', $remoteSettings['opcache']['host']); + } + + return $this->taskSsh($remoteConfig->getHost(), $auth) + ->remoteDirectory($remoteSettings['rootdir'], true) + ->timeout($timeouts['clear_op_cache']) + ->exec((string) $clearOpcache); + } +} diff --git a/src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php b/src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php new file mode 100644 index 0000000..be8836a --- /dev/null +++ b/src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php @@ -0,0 +1,62 @@ +getArgument(('remoteConfig')); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $releaseToCompress = $event->getArgument('releaseToCompress'); + $timeouts = $event->getArgument('timeouts'); + + // Strip the releases dir from the release to compress, so the tar + // contains relative paths. + $relativeReleaseToCompress = str_replace($remoteSettings['releasesdir'] . '/', '', $releaseToCompress); + + return $this->taskSsh($remoteConfig->getHost(), $auth) + ->remoteDirectory($remoteSettings['releasesdir']) + ->exec((string) CommandBuilder::create('tar') + ->addFlag('c') + ->addFlag('z') + ->addFlag('f', $relativeReleaseToCompress . '.tar.gz') + ->addArgument($relativeReleaseToCompress) + ->onSuccess( + CommandBuilder::create('chown') + ->addFlag('R') + ->addArgument($remoteConfig->getUser() . ':' . $remoteConfig->getUser()) + ->addArgument($relativeReleaseToCompress) + ->onSuccess(CommandBuilder::create('chmod') + ->addFlag('R') + ->addArgument('a+rwx') + ->addArgument($relativeReleaseToCompress) + ->onSuccess(CommandBuilder::create('rm') + ->addFlag('rf') + ->addArgument($relativeReleaseToCompress) + ) + ) + ) + ->onFailure( + CommandBuilder::create('rm') + ->addFlag('r') + ->addFlag('f') + ->addArgument($relativeReleaseToCompress . '.tar.gz') + ) + )->timeout($timeouts['compress_old_release']); + } +} diff --git a/src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php b/src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php new file mode 100644 index 0000000..ba29fdd --- /dev/null +++ b/src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php @@ -0,0 +1,58 @@ +getArgument('host'); + $user = $event->getArgument('user'); + $privateKeyFile = $event->getArgument('privateKeyFile'); + $remoteSettings = $event->getArgument('remoteSettings'); + $key = $host . ':' . $user . ':' . $privateKeyFile . ':' . $remoteSettings['releasesdir']; + + $auth = new KeyFile($user, $privateKeyFile); + if (!array_key_exists($key, $this->projectRoots)) { + $fullOutput = ''; + $this->taskSsh($host, $auth) + ->remoteDirectory($remoteSettings['releasesdir'], true) + ->exec( + (string) CommandBuilder::create('ls') + ->addFlag('1') + ->pipeOutputTo( + CommandBuilder::create('sort') + ->addFlag('r') + ->pipeOutputTo( + CommandBuilder::create('head') + ->addFlag('1') + ) + ), + function ($output) use (&$fullOutput) { + $fullOutput .= $output; + } + ) + ->run(); + $this->projectRoots[$key] = $remoteSettings['releasesdir'] . '/' . substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput))); + } + + return $this->projectRoots[$key]; + } +} diff --git a/src/EventHandler/DefaultHandler/DownloadBackupHandler.php b/src/EventHandler/DefaultHandler/DownloadBackupHandler.php new file mode 100644 index 0000000..f5ed367 --- /dev/null +++ b/src/EventHandler/DefaultHandler/DownloadBackupHandler.php @@ -0,0 +1,48 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $options = $event->getArgument('options'); + + if (!$options['files'] && !$options['data']) { + $options['files'] = true; + $options['data'] = true; + } + $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time']; + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $collection = $this->collectionBuilder(); + $collection + ->taskSFTP($remoteConfig->getHost(), $auth); + + // Download files. + if ($options['files']) { + $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']); + $collection->get($backupDir . '/' . $filesBackupFile, $filesBackupFile); + } + + // Download data. + if ($options['data']) { + $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']); + $collection->get($backupDir . '/' . $dbBackupFile, $dbBackupFile); + } + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/InstallHandler.php b/src/EventHandler/DefaultHandler/InstallHandler.php new file mode 100644 index 0000000..0aeb710 --- /dev/null +++ b/src/EventHandler/DefaultHandler/InstallHandler.php @@ -0,0 +1,19 @@ +collectionBuilder(); + } +} diff --git a/src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php b/src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php new file mode 100644 index 0000000..c333aa3 --- /dev/null +++ b/src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php @@ -0,0 +1,39 @@ +siteInstalled)) { + return $this->siteInstalled; + } + + /** @var RemoteConfig $remoteConfig */ + $remoteConfig = $event->getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $currentWebRoot = $remoteSettings['currentdir']; + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $result = $this->taskSsh($remoteConfig->getHost(), $auth) + ->remoteDirectory($currentWebRoot, true) + ->exec('ls -al | grep index.php') + ->run(); + $this->siteInstalled = $result->wasSuccessful(); + + return $this->siteInstalled; + } +} diff --git a/src/EventHandler/DefaultHandler/LocalSettingsHandler.php b/src/EventHandler/DefaultHandler/LocalSettingsHandler.php new file mode 100644 index 0000000..bb65f81 --- /dev/null +++ b/src/EventHandler/DefaultHandler/LocalSettingsHandler.php @@ -0,0 +1,36 @@ +readProperties(); + $app = $event->getArgument('app'); + $timestamp = $event->getArgument('timestamp'); + $defaults = [ + 'app' => $app, + 'time' => is_null($timestamp) ? $this->time : $timestamp, + 'project_root' => $this->getConfig()->get('digipolis.root.project'), + 'web_root' => $this->getConfig()->get('digipolis.root.web'), + 'filesdir' => 'files', + ]; + + // Set up destination config. + $replacements = array( + '[project_root]' => $this->getConfig()->get('digipolis.root.project'), + '[web_root]' => $this->getConfig()->get('digipolis.root.web'), + '[app]' => $app, + '[time]' => is_null($timestamp) ? $this->time : $timestamp, + ); + + return ($this->tokenReplace($this->getConfig()->get('local'), $replacements) ?? []) + $defaults; + } +} diff --git a/src/EventHandler/DefaultHandler/MirrorDirHandler.php b/src/EventHandler/DefaultHandler/MirrorDirHandler.php new file mode 100644 index 0000000..574649c --- /dev/null +++ b/src/EventHandler/DefaultHandler/MirrorDirHandler.php @@ -0,0 +1,45 @@ +getArgument('dir'); + $destination = $event->getArgument('destination'); + if (!is_dir($dir)) { + return $this->collectionBuilder(); + } + $task = $this->taskFilesystemStack(); + $task->mkdir($destination); + + $directoryIterator = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS); + $recursiveIterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST); + foreach ($recursiveIterator as $item) { + $destinationFile = $destination . '/' . $recursiveIterator->getSubPathName(); + if (file_exists($destinationFile)) { + continue; + } + if (is_link($item)) { + if ($item->getRealPath() !== false) { + $task->symlink($item->getLinkTarget(), $destinationFile); + } + continue; + } + if ($item->isDir()) { + $task->mkdir($destinationFile); + continue; + } + $task->copy($item, $destinationFile); + } + return $task; + } +} diff --git a/src/EventHandler/DefaultHandler/PostSymlinkHandler.php b/src/EventHandler/DefaultHandler/PostSymlinkHandler.php new file mode 100644 index 0000000..b6d9612 --- /dev/null +++ b/src/EventHandler/DefaultHandler/PostSymlinkHandler.php @@ -0,0 +1,52 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $timeouts = $event->getArgument('timeouts'); + + $collection = $this->collectionBuilder(); + if (isset($remoteSettings['postsymlink_filechecks']) && $remoteSettings['postsymlink_filechecks']) { + $projectRoot = $remoteSettings['rootdir']; + $collection->taskSsh($remoteConfig->getHost(), $auth) + ->remoteDirectory($projectRoot, true) + ->timeout($timeouts['post_symlink']); + foreach ($remoteSettings['postsymlink_filechecks'] as $file) { + // If this command fails, the collection will fail, which will + // trigger a rollback. + $builder = CommandBuilder::create('ls') + ->addArgument($file) + ->pipeOutputTo('grep') + ->addArgument($file) + ->onFailure( + CommandBuilder::create('echo') + ->addArgument('[ERROR] ' . $file . ' was not found.') + ->onFinished('exit') + ->addArgument('1') + ); + $collection->exec((string) $builder); + } + } + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php b/src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php new file mode 100644 index 0000000..0a121cb --- /dev/null +++ b/src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php @@ -0,0 +1,34 @@ +getArgument('localSettings'); + + return $this + ->taskExecStack() + ->exec( + (string) CommandBuilder::create('chown') + ->addFlag('R') + ->addRawArgument('$USER') + ->addArgument(dirname($localSettings['filesdir'])) + ) + ->exec( + (string) CommandBuilder::create('chmod') + ->addFlag('R') + ->addArgument('u+w') + ->addArgument(dirname($localSettings['filesdir'])) + ); + } +} diff --git a/src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php b/src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php new file mode 100644 index 0000000..5430b39 --- /dev/null +++ b/src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php @@ -0,0 +1,55 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $options = $event->getArgument('options'); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $timeouts = $event->getArgument('timeouts'); + $fileBackupConfig = $event->getArgument('fileBackupConfig'); + + if (!$options['files'] && !$options['data']) { + $options['files'] = true; + $options['data'] = true; + } + if ($options['files']) { + $removeFiles = CommandBuilder::create('rm')->addFlag('rf'); + if (!$fileBackupConfig['file_backup_subdirs']) { + $removeFiles->addArgument('./*'); + $removeFiles->addArgument('./.??*'); + } + foreach ($fileBackupConfig['file_backup_subdirs'] as $subdir) { + $removeFiles->addArgument($subdir . '/*'); + $removeFiles->addArgument($subdir . '/.??*'); + } + + return $this->taskSsh($remoteConfig->getHost(), $auth) + ->remoteDirectory($remoteSettings['filesdir'], true) + // Files dir can be pretty big on large sites. + ->timeout($timeouts['pre_restore']) + ->exec((string) $removeFiles); + } + + return $this->collectionBuilder(); + } +} diff --git a/src/EventHandler/DefaultHandler/PreSymlinkHandler.php b/src/EventHandler/DefaultHandler/PreSymlinkHandler.php new file mode 100644 index 0000000..c3bc89b --- /dev/null +++ b/src/EventHandler/DefaultHandler/PreSymlinkHandler.php @@ -0,0 +1,79 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $timeouts = $event->getArgument('timeouts'); + + $collection = $this->collectionBuilder(); + foreach ($remoteSettings['symlinks'] as $symlink) { + $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($remoteConfig, $symlink, $timeouts['pre_symlink']); + if ($preIndividualSymlinkTask) { + $collection->addTask($preIndividualSymlinkTask); + } + } + + return $collection; + } + + /** + * Tasks to execute before creating an individual symlink. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param string $symlink + * The symlink in format "target:link". + * @param int $timeout + * The SSH timeout in seconds. + * + * @return bool|\Robo\Contract\TaskInterface + * The presymlink task, false if no pre symlink task needs to run. + */ + public function preIndividualSymlinkTask(RemoteConfig $remoteConfig, $symlink, $timeout) + { + $remoteSettings = $remoteConfig->getRemoteSettings(); + $projectRoot = $remoteSettings['rootdir']; + $task = $this->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile())) + ->remoteDirectory($projectRoot, true) + ->timeout($timeout); + list($target, $link) = explode(':', $symlink); + if ($link === $remoteSettings['currentdir']) { + return false; + } + // If the link we're going to create is an existing directory, + // mirror that directory on the symlink target and then delete it + // before creating the symlink + $task->exec( + (string) CommandBuilder::create('vendor/bin/robo digipolis:mirror-dir') + ->addArgument($link) + ->addArgument($target) + ); + $task->exec( + (string) CommandBuilder::create('rm') + ->addFlag('rf') + ->addArgument($link) + ); + + return $task; + } +} diff --git a/src/EventHandler/DefaultHandler/PushPackageHandler.php b/src/EventHandler/DefaultHandler/PushPackageHandler.php new file mode 100644 index 0000000..ef17afe --- /dev/null +++ b/src/EventHandler/DefaultHandler/PushPackageHandler.php @@ -0,0 +1,44 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + $archive = $event->hasArgument('archiveName') && $event->getArgument('archiveName') + ? $event->getArgument('archiveName') + : $remoteSettings['time'] . '.tar.gz'; + $releaseDir = $remoteSettings['releasesdir'] . '/' . $remoteSettings['time']; + + $collection = $this->collectionBuilder(); + $collection->taskPushPackage($remoteConfig->getHost(), $auth) + ->destinationFolder($releaseDir) + ->package($archive); + + $collection->taskSsh($remoteConfig->getHost(), $auth) + ->remoteDirectory($releaseDir, true) + ->exec((string) CommandBuilder::create('chmod') + ->addArgument('u+rx') + ->addArgument('vendor/bin/robo') + ); + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/RealpathHandler.php b/src/EventHandler/DefaultHandler/RealpathHandler.php new file mode 100644 index 0000000..385c38b --- /dev/null +++ b/src/EventHandler/DefaultHandler/RealpathHandler.php @@ -0,0 +1,18 @@ +getArgument('path')); + } +} diff --git a/src/EventHandler/DefaultHandler/RemoteSettingsHandler.php b/src/EventHandler/DefaultHandler/RemoteSettingsHandler.php new file mode 100644 index 0000000..531f585 --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoteSettingsHandler.php @@ -0,0 +1,54 @@ +readProperties(); + $user = $event->getArgument('user'); + $servers = $event->getArgument('servers'); + $privateKeyFile = $event->getArgument('privateKeyFile'); + $app = $event->getArgument('app'); + $timestamp = $event->getArgument('timestamp'); + $defaults = [ + 'user' => $user, + 'private-key' => $privateKeyFile, + 'app' => $app, + 'createbackup' => true, + 'time' => $timestamp, + 'filesdir' => 'files', + ]; + + // Set up destination config. + $replacements = array( + '[user]' => $user, + '[private-key]' => $privateKeyFile, + '[app]' => $app, + '[time]' => $timestamp, + ); + if (is_array($servers)) { + foreach ($servers as $key => $server) { + $replacements['[server-' . $key . ']'] = $server; + $defaults['server-' . $key] = $server; + } + } + + $settings = $this->processEnvironmentOverrides( + ($this->tokenReplace($this->getConfig()->get('remote'), $replacements) ?? []) + $defaults + ); + + // Reverse the symlinks so the `current` symlink is the last one to be + // created. + $settings['symlinks'] = array_reverse($settings['symlinks'] ?? [], true); + + return $settings; + } +} diff --git a/src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php b/src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php new file mode 100644 index 0000000..f0764dc --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php @@ -0,0 +1,33 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + + return $this->taskRemoteSwitchPrevious( + $remoteConfig->getHost(), + $auth, + $remoteConfig->getCurrentProjectRoot(), + $remoteSettings['releasesdir'], + $remoteSettings['currentdir'] + ); + } +} diff --git a/src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php b/src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php new file mode 100644 index 0000000..9a7d40d --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php @@ -0,0 +1,45 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $timeouts = $event->getArgument('timeouts'); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + + $collection = $this->collectionBuilder(); + foreach ($remoteSettings['symlinks'] as $link) { + $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($remoteConfig, $link, $timeouts['symlink']); + if ($preIndividualSymlinkTask) { + $collection->addTask($preIndividualSymlinkTask); + } + list($target, $linkname) = explode(':', $link); + $collection->taskSsh($remoteConfig->getHost(), $auth) + ->exec( + (string) CommandBuilder::create('ln') + ->addFlag('s') + ->addFlag('T') + ->addFlag('f') + ->addArgument($target) + ->addArgument($linkname) + ); + } + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php b/src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php new file mode 100644 index 0000000..f6fc79a --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php @@ -0,0 +1,40 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $timeouts = $event->getArgument('timeouts'); + $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time']; + + $collection = $this->collectionBuilder(); + $collection->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile())) + ->timeout($timeouts['remove_backup']) + ->exec( + (string) CommandBuilder::create('rm') + ->addFlag('rf') + ->addArgument($backupDir) + ); + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php b/src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php new file mode 100644 index 0000000..35dacd5 --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php @@ -0,0 +1,32 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getHost(), $remoteConfig->getPrivateKeyFile()); + $releaseDir = $event->hasArgument('releaseDir') + ? $event->getArgument('releaseDir') + : $remoteSettings['releasesdir'] . '/' . $remoteSettings['time']; + + return $this->taskRemoteRemoveRelease($remoteConfig->getHost(), $auth, null, $releaseDir); + } +} diff --git a/src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php b/src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php new file mode 100644 index 0000000..ea649d4 --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php @@ -0,0 +1,32 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $options = $event->getArgument('options'); + $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']); + $removeLocalBackup = CommandBuilder::create('rm') + ->addFlag('f') + ->addArgument($dbBackupFile); + if ($options['files']) { + $removeLocalBackup->addArgument($this->backupFileName('.tar.gz', $remoteSettings['time'])); + } + + return $this->taskExecStack()->exec((string) $removeLocalBackup); + } +} diff --git a/src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php b/src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php new file mode 100644 index 0000000..ba89638 --- /dev/null +++ b/src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php @@ -0,0 +1,41 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']); + $dbRestore = CommandBuilder::create('vendor/bin/robo digipolis:database-restore')->addOption('source', $dbBackupFile); + $cwd = getcwd(); + + return $this->taskExecStack() + ->exec( + (string) CommandBuilder::create('cd') + ->addArgument($this->getConfig()->get('digipolis.root.project')) + ->onSuccess($dbRestore) + ) + ->exec( + (string) CommandBuilder::create('cd') + ->addArgument($cwd) + ->onSuccess( + CommandBuilder::create('rm') + ->addFlag('rf') + ->addArgument($dbBackupFile) + ) + ); + } +} diff --git a/src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php b/src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php new file mode 100644 index 0000000..cd34ffc --- /dev/null +++ b/src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php @@ -0,0 +1,43 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $localSettings = $event->getArgument('localSettings'); + $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']); + + return $this->taskExecStack() + ->exec( + (string) CommandBuilder::create('rm') + ->addFlag('rf') + ->addArgument($localSettings['filesdir'] . '/*') + ->addArgument($localSettings['filesdir'] . '/.??*') + ) + ->exec( + (string) CommandBuilder::create('tar') + ->addFlag('xkz') + ->addFlag('f', $filesBackupFile) + ->addFlag('C', $localSettings['filesdir']) + ) + ->exec( + (string) CommandBuilder::create('rm') + ->addFlag('f') + ->addArgument($filesBackupFile) + ); + } +} diff --git a/src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php b/src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php new file mode 100644 index 0000000..b361d65 --- /dev/null +++ b/src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php @@ -0,0 +1,66 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $options = $event->getArgument('options'); + $timeouts = $event->getArgument('timeouts'); + + if (!$options['files'] && !$options['data']) { + $options['files'] = true; + $options['data'] = true; + } + + $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time']; + + $collection = $this->collectionBuilder(); + + if ($options['files']) { + $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']); + $collection + ->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile())) + ->remoteDirectory($remoteSettings['filesdir'], true) + ->timeout($timeouts['restore_files_backup']) + ->exec( + (string) CommandBuilder::create('tar') + ->addFlag('xkz') + ->addFlag('f', $backupDir . '/' . $filesBackupFile) + ); + } + + // Restore the db backup. + if ($options['data']) { + $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']); + $collection + ->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile())) + ->remoteDirectory($remoteConfig->getCurrentProjectRoot(), true) + ->timeout($timeouts['restore_db_backup']) + ->exec( + (string) CommandBuilder::create('vendor/bin/robo digipolis:database-restore') + ->addOption('source', $backupDir . '/' . $dbBackupFile) + ); + } + + return $collection; + } +} diff --git a/src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php b/src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php new file mode 100644 index 0000000..c23bbac --- /dev/null +++ b/src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php @@ -0,0 +1,278 @@ +getArgument('sourceRemoteConfig'); + $destinationRemoteConfig = $event->getArgument('destinationRemoteConfig'); + $fileBackupConfig = $event->getArgument('fileBackupConfig'); + $timeouts = $event->getArgument('timeouts'); + + $tmpPrivateKeyFile = '~/.ssh/' . uniqid('robo_', true) . '.id_rsa'; + $collection = $this->collectionBuilder(); + // Generate a temporary key. + $collection->addTask( + $this->generateKeyPair($tmpPrivateKeyFile) + ); + + $collection->completion( + $this->removeKeyPair($tmpPrivateKeyFile) + ); + + // Install it on the destination host. + $collection->addTask( + $this->installPublicKeyOnDestination( + $tmpPrivateKeyFile, + $destinationRemoteConfig + ) + ); + + // Remove it from the destination host when we're done. + $collection->completion( + $this->removePublicKeyFromDestination( + $tmpPrivateKeyFile, + $destinationRemoteConfig + ) + ); + + // Install the private key on the source host. + $collection->addTask( + $this->installPrivateKeyOnSource( + $tmpPrivateKeyFile, + $sourceRemoteConfig + ) + ); + + // Remove the private key from the source host. + $collection->completion( + $this->removePrivateKeyFromSource( + $tmpPrivateKeyFile, + $sourceRemoteConfig + ) + ); + + $dirs = ($fileBackupConfig['file_backup_subdirs'] ? $fileBackupConfig['file_backup_subdirs'] : ['']); + + foreach ($dirs as $dir) { + $dir .= ($dir !== '' ? '/' : ''); + $collection->addTask( + $this->rsyncDirectory( + $dir, + $tmpPrivateKeyFile, + $sourceRemoteConfig, + $destinationRemoteConfig, + $fileBackupConfig, + $timeouts['synctask_rsync'] + ) + ); + } + + return $collection; + } + + + /** + * Generate an SSH key pair. + * + * @param string $privateKeyFile + * Path to store the private key file. + * + * @return \Robo\Contract\TaskInterface + */ + protected function generateKeyPair($privateKeyFile) + { + return $this->taskExec( + (string) CommandBuilder::create('ssh-keygen') + ->addFlag('q') + ->addFlag('t', 'rsa') + ->addFlag('b', 4096) + ->addRawFlag('N', '""') + ->addRawFlag('f', $privateKeyFile) + ->addFlag('C', 'robo:' . md5($privateKeyFile)) + ); + } + + /** + * Remove an SSH key pair. + * + * @param string $privateKeyFile + * Path to store the private key file. + * + * @return \Robo\Contract\TaskInterface + */ + protected function removeKeyPair($privateKeyFile) + { + return $this->taskExecStack() + ->exec( + (string) CommandBuilder::create('rm') + ->addFlag('f') + ->addRawArgument($privateKeyFile) + ->addRawArgument($privateKeyFile . '.pub') + ); + } + + /** + * Install a public SSH key on a host. + * + * @param string $privateKeyFile + * Path to the private key file of the key pair to install. + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function installPublicKeyOnDestination($privateKeyFile, RemoteConfig $remoteConfig) + { + return $this->taskExec( + (string) CommandBuilder::create('cat') + ->addRawArgument($privateKeyFile . '.pub') + ->pipeOutputTo( + CommandBuilder::create('ssh') + ->addArgument($remoteConfig->getUser() . '@' . $remoteConfig->getHost()) + ->addFlag('o', 'StrictHostKeyChecking=no') + ->addRawFlag('i', $remoteConfig->getPrivateKeyFile()) + ) + ->addArgument( + CommandBuilder::create('mkdir') + ->addFlag('p') + ->addRawArgument('~/.ssh') + ->onSuccess( + CommandBuilder::create('cat') + ->chain('~/.ssh/authorized_keys', '>>') + ) + ) + ); + } + + /** + * Remove a public key from a host. + * + * @param string $privateKeyFile + * Path to the private key file of the key pair to remove. + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function removePublicKeyFromDestination($privateKeyFile, RemoteConfig $remoteConfig) + { + return $this->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile())) + ->exec( + (string) CommandBuilder::create('sed') + ->addFlag('i', '/robo:' . md5($privateKeyFile) . '/d') + ->addRawArgument('~/.ssh/authorized_keys') + ); + } + + /** + * Install a private key on a host. + * + * @param string $privateKeyFile + * Private key to install. + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function installPrivateKeyOnSource($privateKeyFile, RemoteConfig $remoteConfig) + { + return $this->taskRsync() + ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $remoteConfig->getPrivateKeyFile() . '`"') + ->fromPath($privateKeyFile) + ->toHost($remoteConfig->getHost()) + ->toUser($remoteConfig->getUser()) + ->toPath('~/.ssh') + ->archive() + ->compress() + ->checksum() + ->wholeFile(); + } + + /** + * Remove a private key from a host. + * + * @param string $privateKeyFile + * Path to the private key file of the key pair to remove. + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function removePrivateKeyFromSource($privateKeyFile, RemoteConfig $remoteConfig) + { + return $this->taskSsh($remoteConfig->getHost(), new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile())) + ->exec( + (string) CommandBuilder::create('rm') + ->addFlag('f') + ->addRawArgument($privateKeyFile) + ); + } + + /** + * Rsync a directory between hosts. + * + * @param string $directory + * The directory to sync. + * @param string $privateKeyFile + * The path the the private key of the keypair installed on src and dest. + * @param RemoteConfig $sourceRemoteConfig + * RemoteConfig object populated with data relevant to the source. + * @param RemoteConfig $destinationRemoteConfig + * RemoteConfig object populated with data relevant to the destination. + * @param array $fileBackupConfig + * File backup config. + * @param int $timeout + * Timeout setting for the sync. + * + * @return \Robo\Contract\TaskInterface + */ + protected function rsyncDirectory( + $directory, + $privateKeyFile, + RemoteConfig $sourceRemoteConfig, + RemoteConfig $destinationRemoteConfig, + $fileBackupConfig, + $timeout + ) { + $sourceRemoteSettings = $sourceRemoteConfig->getRemoteSettings(); + $destinationRemoteSettings = $destinationRemoteConfig->getRemoteSettings(); + $rsync = $this->taskRsync() + ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `cd -P ' . $sourceRemoteConfig->getCurrentProjectRoot() . ' && vendor/bin/robo digipolis:realpath ' . $privateKeyFile . '`"') + ->fromPath($sourceRemoteSettings['filesdir'] . '/' . $directory) + ->toHost($destinationRemoteConfig->getHost()) + ->toUser($destinationRemoteConfig->getUser()) + ->toPath($destinationRemoteSettings['filesdir'] . '/' . $directory) + ->archive() + ->delete() + ->rawArg('--copy-links --keep-dirlinks') + ->compress() + ->checksum() + ->wholeFile(); + foreach ($fileBackupConfig['exclude_from_backup'] as $exclude) { + $rsync->exclude($exclude); + } + + return $this->taskSsh($sourceRemoteConfig->getHost(), new KeyFile($sourceRemoteConfig->getUser(), $sourceRemoteConfig->getPrivateKeyFile())) + ->timeout($timeout) + ->exec($rsync); + } +} diff --git a/src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php b/src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php new file mode 100644 index 0000000..2f4df15 --- /dev/null +++ b/src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php @@ -0,0 +1,43 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $localSettings = $event->getArgument('localSettings'); + $directory = $event->getArgument('directory'); + $fileBackupConfig = $event->getArgument('fileBackupConfig'); + + $rsync = $this->taskRsync() + ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $remoteConfig->getPrivateKeyFile() . '`"') + ->fromHost($remoteConfig->getHost()) + ->fromUser($remoteConfig->getUser()) + ->fromPath($remoteSettings['filesdir'] . '/' . $directory) + ->toPath($localSettings['filesdir'] . '/' . $directory) + ->archive() + ->delete() + ->rawArg('--copy-links --keep-dirlinks') + ->compress() + ->checksum() + ->wholeFile(); + + foreach ($fileBackupConfig['exclude_from_backup'] as $exclude) { + $rsync->exclude($exclude); + } + + return $rsync; + } +} diff --git a/src/EventHandler/DefaultHandler/SettingsHandler.php b/src/EventHandler/DefaultHandler/SettingsHandler.php new file mode 100644 index 0000000..c6b26bc --- /dev/null +++ b/src/EventHandler/DefaultHandler/SettingsHandler.php @@ -0,0 +1,110 @@ + 'HOSTNAME', + 'environment_matcher' => '\\DigipolisGent\\Robo\\Helpers\\Util\\EnvironmentMatcher::regexMatch', + ]; + + /** + * Process environment-specific overrides. + * + * @param array $settings + * @return array + */ + protected function processEnvironmentOverrides($settings) + { + $settings += static::$defaultEnvironmentOverrideSettings; + if (!isset($settings['environment_overrides']) || !$settings['environment_overrides']) { + return $settings; + } + + $server = $this->getFirstServer($settings); + if (!$server) { + return $settings; + } + + // Parse the env var on the server. + $auth = new KeyFile($settings['user'], $settings['private-key']); + $fullOutput = ''; + $this->taskSsh($server, $auth) + ->exec( + (string) CommandBuilder::create('echo') + ->addRawArgument('$' . $settings['environment_env_var']), + function ($output) use (&$fullOutput) { + $fullOutput .= $output; + } + ) + ->run(); + $envVarValue = substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput))); + foreach ($settings['environment_overrides'] as $environmentMatch => $overrides) { + if (call_user_func($settings['environment_matcher'], $environmentMatch, $envVarValue)) { + $settings = ArrayMerger::doMerge($settings, $overrides); + } + } + + return $settings; + } + + /** + * Get the first server entry from the remote settings. + * + * @param array $settings + * + * @return string|bool + * First server if found, false otherwise. + * + * @see self::processEnvironmentOverrides + */ + protected function getFirstServer($settings) + { + foreach ($settings as $key => $value) { + if (preg_match('/^server/', $key) === 1) { + return $value; + } + } + + return false; + } + + /** + * Helper functions to replace tokens in an array. + * + * @param string|array $input + * The array or string containing the tokens to replace. + * @param array $replacements + * The token replacements. + * + * @return string|array + * The input with the tokens replaced with their values. + */ + protected function tokenReplace($input, $replacements) + { + if (is_string($input)) { + return strtr($input, $replacements); + } + if (is_scalar($input) || empty($input)) { + return $input; + } + foreach ($input as &$i) { + $i = $this->tokenReplace($i, $replacements); + } + + return $input; + } +} diff --git a/src/EventHandler/DefaultHandler/SwitchPreviousHandler.php b/src/EventHandler/DefaultHandler/SwitchPreviousHandler.php new file mode 100644 index 0000000..c23ae60 --- /dev/null +++ b/src/EventHandler/DefaultHandler/SwitchPreviousHandler.php @@ -0,0 +1,23 @@ +getArgument('releasesDir'); + $currentSymlink = $event->getArgument('currentSymlink'); + + return $this->taskSwitchPrevious($releasesDir, $currentSymlink); + } +} diff --git a/src/EventHandler/DefaultHandler/TimeoutSettingHandler.php b/src/EventHandler/DefaultHandler/TimeoutSettingHandler.php new file mode 100644 index 0000000..a4338f0 --- /dev/null +++ b/src/EventHandler/DefaultHandler/TimeoutSettingHandler.php @@ -0,0 +1,74 @@ +getArgument('type'); + $timeoutSettings = $this->getTimeoutSettings(); + return isset($timeoutSettings[$type]) ? $timeoutSettings[$type] : static::DEFAULT_TIMEOUT; + } + + /** + * Timeouts can be overwritten in properties.yml under the `timeout` key. + * + * @param string $setting + * + * @return int + */ + public function getTimeoutSetting($setting) + { + $timeoutSettings = $this->getTimeoutSettings(); + return isset($timeoutSettings[$setting]) ? $timeoutSettings[$setting] : static::DEFAULT_TIMEOUT; + } + + /** + * Get all timeout settings. + * + * @return array + */ + protected function getTimeoutSettings() + { + $this->readProperties(); + return $this->getConfig()->get('timeouts', []) + $this->getDefaultTimeoutSettings(); + } + + /** + * Get the default timeout settings. + * + * @return array + */ + protected function getDefaultTimeoutSettings() + { + // Refactor this to default.properties.yml + return [ + 'presymlink_mirror_dir' => 60, + 'synctask_rsync' => 1800, + 'backup_files' => 300, + 'backup_database' => 300, + 'remove_backup' => 300, + 'restore_files_backup' => 300, + 'restore_db_backup' => 60, + 'pre_restore' => 300, + 'clean_dir' => 30, + 'clear_op_cache' => 30, + 'compress_old_release' => 300, + ]; + } +} diff --git a/src/EventHandler/DefaultHandler/UpdateHandler.php b/src/EventHandler/DefaultHandler/UpdateHandler.php new file mode 100644 index 0000000..cf059c0 --- /dev/null +++ b/src/EventHandler/DefaultHandler/UpdateHandler.php @@ -0,0 +1,19 @@ +collectionBuilder(); + } +} diff --git a/src/EventHandler/DefaultHandler/UploadBackupHandler.php b/src/EventHandler/DefaultHandler/UploadBackupHandler.php new file mode 100644 index 0000000..f8e9fe0 --- /dev/null +++ b/src/EventHandler/DefaultHandler/UploadBackupHandler.php @@ -0,0 +1,48 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $options = $event->getArgument('options'); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + + if (!$options['files'] && !$options['data']) { + $options['files'] = true; + $options['data'] = true; + } + $backupDir = $remoteSettings['backupsdir'] . '/' . $remoteSettings['time']; + $dbBackupFile = $this->backupFileName('.sql.gz', $remoteSettings['time']); + $filesBackupFile = $this->backupFileName('.tar.gz', $remoteSettings['time']); + + $collection = $this->collectionBuilder(); + $collection + ->taskSsh($remoteConfig->getHost(), $auth) + ->exec((string) CommandBuilder::create('mkdir')->addFlag('p')->addArgument($backupDir)) + ->taskSFTP($remoteConfig->getHost(), $auth); + if ($options['files']) { + $collection->put($backupDir . '/' . $filesBackupFile, $filesBackupFile); + } + if ($options['data']) { + $collection->put($backupDir . '/' . $dbBackupFile, $dbBackupFile); + } + + return $collection; + } +} diff --git a/src/EventHandler/EventHandlerWithPriority.php b/src/EventHandler/EventHandlerWithPriority.php new file mode 100644 index 0000000..c94dc35 --- /dev/null +++ b/src/EventHandler/EventHandlerWithPriority.php @@ -0,0 +1,27 @@ +addShared(AbstractApp::class, [$this->getAppTaskFactoryClass(), 'create'])->addArgument($container); - $container->addServiceProvider(new ServiceProvider()); - - // Inject all our dependencies. - $this->setRemoteHelper($container->get(RemoteHelper::class)); - $this->setBackupTaskFactory($container->get(Backup::class)); - - return $this; - } - - abstract public function getAppTaskFactoryClass(); - - /** - * @return FilesystemStack - */ - protected function taskFilesystemStack() - { - return $this->task(FilesystemStack::class); - } - - /** - * Mirror a directory. - * - * @param string $dir - * Path of the directory to mirror. - * @param string $destination - * Path of the directory where $dir should be mirrored. - * - * @return \Robo\Contract\TaskInterface - * The mirror dir task. - * - * @command digipolis:mirror-dir - */ - public function digipolisMirrorDir(ConsoleIO $io, $dir, $destination) - { - if (!is_dir($dir)) { - return; - } - $task = $this->taskFilesystemStack(); - $task->mkdir($destination); - - $directoryIterator = new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS); - $recursiveIterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST); - foreach ($recursiveIterator as $item) { - $destinationFile = $destination . '/' . $recursiveIterator->getSubPathName(); - if (file_exists($destinationFile)) { - continue; - } - if (is_link($item)) { - if ($item->getRealPath() !== false) { - $task->symlink($item->getLinkTarget(), $destinationFile); - } - continue; - } - if ($item->isDir()) { - $task->mkdir($destinationFile); - continue; - } - $task->copy($item, $destinationFile); - } - return $task; - } - - /** - * Polyfill for realpath. - * - * @param string $path - * - * @return string - * - * @command digipolis:realpath - */ - public function digipolisRealpath($path) - { - return Path::realpath($path); - } - - /** - * Switch the current release symlink to the previous release. - * - * @param string $releasesDir - * Path to the folder containing all releases. - * @param string $currentSymlink - * Path to the current release symlink. - * - * @command digipolis:switch-previous - */ - public function digipolisSwitchPrevious($releasesDir, $currentSymlink) - { - return $this->taskSwitchPrevious($releasesDir, $currentSymlink); - } - - /** - * Sync the database and files to your local environment. - * - * @param string $host - * IP address of the source server. - * @param string $user - * SSH user to connect to the source server. - * @param string $keyFile - * Private key file to use to connect to the source server. - * @param array $opts - * Command options - * - * @option app The name of the app we're syncing. - * @option files Sync only files. - * @option data Sync only the database. - * @option rsync Sync the files via rsync. - * - * @return \Robo\Contract\TaskInterface - * The sync task. - */ - public function digipolisSyncLocal( - $host, - $user, - $keyFile, - $opts = [ - 'app' => 'default', - 'files' => false, - 'data' => false, - 'rsync' => true, - ] - ) { - if (!$opts['files'] && !$opts['data']) { - $opts['files'] = true; - $opts['data'] = true; - } - - $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync']; - - $remote = $this->remoteHelper->getRemoteSettings($host, $user, $keyFile, $opts['app']); - $local = $this->remoteHelper->getLocalSettings($opts['app']); - $auth = new KeyFile($user, $keyFile); - $collection = $this->collectionBuilder(); - - if ($opts['files']) { - $collection - ->taskExecStack() - ->exec( - (string) CommandBuilder::create('chown') - ->addFlag('R') - ->addRawArgument('$USER') - ->addArgument(dirname($local['filesdir'])) - ) - ->exec( - (string) CommandBuilder::create('chmod') - ->addFlag('R') - ->addArgument('u+w') - ->addArgument(dirname($local['filesdir'])) - ); - - if ($opts['rsync']) { - $opts['files'] = false; - - $backupConfig = $this->getBackupConfig(); - $dirs = ($backupConfig['file_backup_subdirs'] ? $backupConfig['file_backup_subdirs'] : ['']); - - foreach ($dirs as $dir) { - $dir .= ($dir !== '' ? '/' : ''); - - $rsync = $this->taskRsync() - ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $keyFile . '`"') - ->fromHost($host) - ->fromUser($user) - ->fromPath($remote['filesdir'] . '/' . $dir) - ->toPath($local['filesdir'] . '/' . $dir) - ->archive() - ->delete() - ->rawArg('--copy-links --keep-dirlinks') - ->compress() - ->checksum() - ->wholeFile(); - - $backupConfig = $this->getBackupConfig(); - foreach ($backupConfig['exclude_from_backup'] as $exclude) { - $rsync->exclude($exclude); - } - - $collection->addTask($rsync); - } - } - } - - if ($opts['data'] || $opts['files']) { - // Create a backup. - $collection->addTask( - $this->backupTaskFactory->backupTask( - $host, - $auth, - $remote, - $opts - ) - ); - // Download the backup. - $collection->addTask( - $this->backupTaskFactory->downloadBackupTask( - $host, - $auth, - $remote, - $opts - ) - ); - } - - if ($opts['files']) { - // Restore the files backup. - $filesBackupFile = $this->backupTaskFactory->backupFileName('.tar.gz', $remote['time']); - $collection - ->exec( - (string) CommandBuilder::create('rm') - ->addFlag('rf') - ->addArgument($local['filesdir'] . '/*') - ->addArgument($local['filesdir'] . '/.??*') - ) - ->exec( - (string) CommandBuilder::create('tar') - ->addFlag('xkz') - ->addFlag('f', $filesBackupFile) - ->addFlag('C', $local['filesdir']) - ) - ->exec( - (string) CommandBuilder::create('rm') - ->addFlag('f') - ->addArgument($filesBackupFile) - ); - } - - if ($opts['data']) { - // Restore the db backup. - $dbBackupFile = $this->backupTaskFactory->backupFileName('.sql.gz', $remote['time']); - $dbRestore = CommandBuilder::create('vendor/bin/robo digipolis:database-restore')->addOption('source', $dbBackupFile); - $cwd = getcwd(); - - $collection->taskExecStack(); - $collection->exec( - (string) CommandBuilder::create('cd') - ->addArgument($this->getConfig()->get('digipolis.root.project')) - ->onSuccess($dbRestore) - ); - $collection->exec( - (string) CommandBuilder::create('cd') - ->addArgument($cwd) - ->onSuccess( - CommandBuilder::create('rm') - ->addFlag('rf') - ->addArgument($dbBackupFile) - ) - ); - } - - return $collection; - } -} diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands.php b/src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands.php new file mode 100644 index 0000000..f632e99 --- /dev/null +++ b/src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands.php @@ -0,0 +1,361 @@ + false, + 'worker' => null, + 'app' => 'default', + ] + ) { + return $this->deploy($arguments, $opts); + } +} diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersMirrorDirCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersMirrorDirCommand.php new file mode 100644 index 0000000..7343f58 --- /dev/null +++ b/src/Robo/Plugin/Commands/DigipolisHelpersMirrorDirCommand.php @@ -0,0 +1,35 @@ +handleTaskEvent( + 'digipolis:mirror-dir', + ['dir' => $dir, 'destination' => $destination] + ); + } +} diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersRealPathCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersRealPathCommand.php new file mode 100644 index 0000000..9807329 --- /dev/null +++ b/src/Robo/Plugin/Commands/DigipolisHelpersRealPathCommand.php @@ -0,0 +1,33 @@ +handleEvent( + 'digipolis:realpath', + ['path' => $path] + ); + + return reset($results); + } +} diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersSwitchPreviousCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersSwitchPreviousCommand.php new file mode 100644 index 0000000..c8be5f6 --- /dev/null +++ b/src/Robo/Plugin/Commands/DigipolisHelpersSwitchPreviousCommand.php @@ -0,0 +1,32 @@ +handleTaskEvent( + 'digipolis:switch-previous', + ['releasesDir' => $releasesDir, 'currentSymlink' => $currentSymlink] + ); + } +} diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php new file mode 100644 index 0000000..c138b3d --- /dev/null +++ b/src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php @@ -0,0 +1,67 @@ + false, 'data' => false, 'rsync' => true] + ) { + return $this->sync( + $sourceUser, + $sourceHost, + $sourcePrivateKeyFile, + $destinationUser, + $destinationHost, + $destinationPrivateKeyFile, + $sourceApp, + $destinationApp, + $opts + ); + } +} diff --git a/src/Robo/Plugin/Commands/DigipolisHelpersSyncLocalCommand.php b/src/Robo/Plugin/Commands/DigipolisHelpersSyncLocalCommand.php new file mode 100644 index 0000000..dcc6645 --- /dev/null +++ b/src/Robo/Plugin/Commands/DigipolisHelpersSyncLocalCommand.php @@ -0,0 +1,129 @@ + 'default', + 'files' => false, + 'data' => false, + 'rsync' => true, + ] + ) { + if (!$opts['files'] && !$opts['data']) { + $opts['files'] = true; + $opts['data'] = true; + } + + $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync']; + + $remoteSettings = $this->getRemoteSettings($host, $user, $privateKeyFile, $opts['app']); + $currentProjectRoot = $this->getCurrentProjectRoot($host, $user, $privateKeyFile, $remoteSettings); + $remoteConfig = new RemoteConfig($host, $user, $privateKeyFile, $remoteSettings, $currentProjectRoot); + $localSettings = $this->getLocalSettings($opts['app']); + $collection = $this->collectionBuilder(); + + if ($opts['files']) { + $collection->addTask($this->handleTaskEvent( + 'digipolis:pre-local-sync-files', + [ + 'localSettings' => $localSettings, + 'remoteConfig' => $remoteConfig, + ] + )); + + $fileBackupConfig = $this->getFileBackupConfig(); + if ($opts['rsync']) { + $opts['files'] = false; + $dirs = ($fileBackupConfig['file_backup_subdirs'] ? $fileBackupConfig['file_backup_subdirs'] : ['']); + + foreach ($dirs as $dir) { + $dir .= ($dir !== '' ? '/' : ''); + $collection->addTask($this->handleTaskEvent( + 'digipolis:rsync-files-to-local', + [ + 'remoteConfig' => $remoteConfig, + 'localSettings' => $localSettings, + 'directory' => $dir, + 'fileBackupConfig' => $fileBackupConfig, + ] + )); + } + } + } + + if ($opts['data'] || $opts['files']) { + // Create the backup on the server. + $collection->addTask($this->backupRemoteTask($remoteConfig, $opts)); + + // Download the backup. + $collection->addTask($this->downloadBackupTask($remoteConfig, $opts)); + } + + if ($opts['files']) { + // Restore the files backup. + $collection->addTask($this->handleTaskEvent( + 'digipolis:restore-backup-files-local', + [ + 'remoteConfig' => $remoteConfig, + 'localSettings' => $localSettings, + ] + )); + + } + + if ($opts['data']) { + // Restore the db backup. + $collection->addTask($this->handleTaskEvent( + 'digipolis:restore-backup-db-local', + [ + 'remoteConfig' => $remoteConfig, + 'localSettings' => $localSettings, + ] + )); + } + + return $collection; + } +} diff --git a/src/Robo/Plugin/Tasks/Remote.php b/src/Robo/Plugin/Tasks/Remote.php index 9e0ab33..757b69a 100644 --- a/src/Robo/Plugin/Tasks/Remote.php +++ b/src/Robo/Plugin/Tasks/Remote.php @@ -10,6 +10,7 @@ abstract class Remote extends BaseTask implements BuilderAwareInterface { use \Robo\Common\BuilderAwareTrait; + /** * The SSH host. * diff --git a/src/RoboFile.php b/src/RoboFile.php new file mode 100644 index 0000000..cb4b743 --- /dev/null +++ b/src/RoboFile.php @@ -0,0 +1,5 @@ +handleTaskEvent( + 'digipolis:backup-remote', + [ + 'remoteConfig' => $remoteConfig, + 'fileBackupConfig' => $this->getFileBackupConfig(), + 'options' => $backupOpts, + 'timeouts' => [ + 'backup_files' => $this->getTimeoutSetting('backup_files'), + 'backup_database' => $this->getTimeoutSetting('backup_database'), + ], + ] + ); + } + + /** + * Get the task that will execute tasks before restoring a backup. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param array $backupOpts + * Extra options for restoring the backup. + * + * @return \Robo\Contract\TaskInterface + */ + protected function preRestoreBackupRemoteTask(RemoteConfig $remoteConfig, $backupOpts) + { + return $this->handleTaskEvent( + 'digipolis:pre-restore-backup-remote', + [ + 'remoteConfig' => $remoteConfig, + 'fileBackupConfig' => $this->getFileBackupConfig(), + 'options' => $backupOpts, + 'timeouts' => [ + 'pre_restore' => $this->getTimeoutSetting('pre_restore'), + ], + ] + ); + } + + /** + * Get the task that will restore a backup. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param array $backupOpts + * Extra options for restoring the backup. + * + * @return \Robo\Contract\TaskInterface + */ + protected function restoreBackupRemoteTask(RemoteConfig $remoteConfig, $backupOpts) + { + return $this->handleTaskEvent( + 'digipolis:restore-backup-remote', + [ + 'remoteConfig' => $remoteConfig, + 'fileBackupConfig' => $this->getFileBackupConfig(), + 'options' => $backupOpts, + 'timeouts' => [ + 'restore_files_backup' => $this->getTimeoutSetting('restore_files_backup'), + 'restore_db_backup' => $this->getTimeoutSetting('restore_db_backup'), + ], + ] + ); + } + + /** + * Get the task that will download a backup from a host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param array $backupOpts + * Extra options that were used for creating the backup. + * + * @return \Robo\Contract\TaskInterface + */ + protected function downloadBackupTask(RemoteConfig $remoteConfig, $backupOpts) + { + return $this->handleTaskEvent( + 'digipolis:download-backup', + [ + 'remoteConfig' => $remoteConfig, + 'options' => $backupOpts, + ] + ); + } + + /** + * Get the task that will upload a backup to a host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param array $backupOpts + * Extra options that were used for creating the backup. + * + * @return \Robo\Contract\TaskInterface + */ + protected function uploadBackupTask(RemoteConfig $remoteConfig, $backupOpts) + { + return $this->handleTaskEvent( + 'digipolis:upload-backup', + [ + 'remoteConfig' => $remoteConfig, + 'options' => $backupOpts, + ] + ); + } + + /** + * Get the task that will remove a backup from a host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param array $backupOpts + * Extra options that were used for creating the backup. + * + * @return \Robo\Contract\TaskInterface + */ + protected function removeBackupRemoteTask(RemoteConfig $remoteConfig, $backupOpts) + { + return $this->handleTaskEvent( + 'digipolis:remove-backup-remote', + [ + 'remoteConfig' => $remoteConfig, + 'options' => $backupOpts, + 'timeouts' => [ + 'remove_backup' => $this->getTimeoutSetting('remove_backup'), + ], + ] + ); + } +} diff --git a/src/Traits/DigipolisHelpersCommandUtilities.php b/src/Traits/DigipolisHelpersCommandUtilities.php new file mode 100644 index 0000000..8acace1 --- /dev/null +++ b/src/Traits/DigipolisHelpersCommandUtilities.php @@ -0,0 +1,224 @@ +getTime() : $timestamp; + $servers = (array) $servers; + $serversCopy = $servers; + sort($serversCopy); + $serversKey = implode('_', $serversCopy); + $cacheKeyParts = [$serversKey, $user, $privateKeyFile, $app, $timestamp]; + $cacheKey = implode(':', $cacheKeyParts); + if (!isset($this->remoteSettingsCache[$cacheKey])) { + $results = $this->handleEvent( + 'digipolis:get-remote-settings', + [ + 'servers' => $servers, + 'user' => $user, + 'privateKeyFile' => $privateKeyFile, + 'app' => $app, + 'timestamp' => $timestamp, + ] + ); + $settings = array_shift($results); + while ($results) { + $settings = ArrayMerger::doMerge($settings, array_shift($results)); + } + $this->remoteSettingsCache[$cacheKey] = $settings; + } + + return $this->remoteSettingsCache[$cacheKey]; + } + + /** + * Get the settings from the 'local' config key, with the tokens replaced. + * + * @param string $app + * The name of the app these settings apply to. + * @param string|null $timestamp + * The timestamp to use. Defaults to the request time. + * + * @return array + * The settings for the local environment and app. + */ + protected function getLocalSettings($app, $timestamp = null) + { + $timestamp = is_null($timestamp) ? TimeHelper::getInstance()->getTime() : $timestamp; + $cacheKey = $app . ':' . $timestamp; + if (!isset($this->localSettingsCache[$cacheKey])) { + $results = $this->handleEvent( + 'digipolis:get-local-settings', + [ + 'app' => $app, + 'timestamp' => $timestamp, + ] + ); + $settings = array_shift($results); + while ($results) { + $settings = ArrayMerger::doMerge($settings, array_shift($results)); + } + $this->localSettingsCache[$cacheKey] = $settings; + } + + return $this->localSettingsCache[$cacheKey]; + } + + /** + * Gets the config for file backups. + * + * @return array + * The backup config with keys file_backup_subdirs and exclude_from_backup + */ + protected function getFileBackupConfig() + { + $configs = $this->handleEvent('digipolis:file-backup-config', []); + $config = [ + 'file_backup_subdirs' => [], + 'exclude_from_backup' => [], + ]; + while ($configs) { + $config = ArrayMerger::doMerge($config, array_shift($configs)); + } + + return $config; + } + + /** + * Get an ssh timeout setting. + * + * @param string $type + * The type to get the setting for. + * + * @return int + * The timeout in seconds. + */ + protected function getTimeoutSetting($type) + { + $settings = $this->handleEvent('digipolis:timeout-setting', ['type' => $type]); + return max($settings); + } + + /** + * Get the project root of the current release on the host. + * + * @param string $host + * The host ip. + * @param string $user + * The ssh user. + * @param string $privateKeyFile + * The path to the private ssh key. + * @param array $remoteSettings + * The remote settings as returned by static::getRemoteSettings(). + * + * @return string + * The path to the project root on the server. + */ + protected function getCurrentProjectRoot($host, $user, $privateKeyFile, $remoteSettings) + { + $results = $this->handleEvent( + 'digipolis:current-project-root', + [ + 'host' => $host, + 'user' => $user, + 'privateKeyFile' => $privateKeyFile, + 'remoteSettings' => $remoteSettings, + ] + ); + + return reset($results); + } + + /** + * Check if a site is already installed + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return bool + * Whether or not the site is installed. + */ + protected function isSiteInstalled(RemoteConfig $remoteConfig) + { + $results = $this->handleEvent( + 'digipolis:is-site-installed', + [ + 'remoteConfig' => $remoteConfig, + ] + ); + + return reset($results); + } + + /** + * Check if the current release has robo available. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return bool + */ + protected function currentReleaseHasRobo(RemoteConfig $remoteConfig) + { + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + return $this->taskSsh($remoteConfig->getHost(), $auth) + ->remoteDirectory($remoteConfig->getCurrentProjectRoot(), true) + ->exec( + (string) CommandBuilder::create('ls') + ->addArgument('vendor/bin/robo') + ->pipeOutputTo( + CommandBuilder::create('grep') + ->addArgument('robo') + ) + ) + ->run() + ->wasSuccessful(); + } + + /** + * Get the task that will clear the cache on the host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function clearCacheTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:clear-cache', + [ + 'remoteConfig' => $remoteConfig, + ] + ); + } +} diff --git a/src/Traits/DigipolisHelpersDeployCommandUtilities.php b/src/Traits/DigipolisHelpersDeployCommandUtilities.php new file mode 100644 index 0000000..7bfa606 --- /dev/null +++ b/src/Traits/DigipolisHelpersDeployCommandUtilities.php @@ -0,0 +1,371 @@ + false, + 'worker' => null, + 'app' => 'default', + ] + ) { + // Define variables. + $opts += ['force-install' => false]; + $privateKeyFile = array_pop($arguments); + $user = array_pop($arguments); + $servers = $arguments; + $worker = is_null($opts['worker']) ? reset($servers) : $opts['worker']; + $remoteSettings = $this->getRemoteSettings($servers, $user, $privateKeyFile, $opts['app']); + $workerCurrentProjectRoot = $this->getCurrentProjectRoot($worker, $user, $privateKeyFile, $remoteSettings); + $releaseDir = $remoteSettings['releasesdir'] . '/' . $remoteSettings['time']; + $archive = $remoteSettings['time'] . '.tar.gz'; + $backupOpts = ['files' => false, 'data' => true]; + $workerRemoteConfig = new RemoteConfig($worker, $user, $privateKeyFile, $remoteSettings, $workerCurrentProjectRoot); + + $collection = $this->collectionBuilder(); + + // Build the archive to deploy. + $collection->addTask($this->buildTask($archive)); + + // Create a backup and a rollback task if a site is already installed. + if ( + $remoteSettings['createbackup'] + && $this->isSiteInstalled($workerRemoteConfig) + && $this->currentReleaseHasRobo($workerRemoteConfig) + ) { + // Create a backup. + $collection->addTask($this->backupRemoteTask($workerRemoteConfig, $backupOpts)); + + // Create a rollback for this backup for when the deploy fails. + $collection->rollback($this->preRestoreBackupRemoteTask($workerRemoteConfig, $backupOpts)); + $collection->rollback($this->restoreBackupRemoteTask($workerRemoteConfig, $backupOpts)); + } + + // Push the package to the servers and create the required symlinks. + foreach ($servers as $server) { + $serverProjectRoot = $this->getCurrentProjectRoot($server, $user, $privateKeyFile, $remoteSettings); + $serverRemoteConfig = new RemoteConfig($server, $user, $privateKeyFile, $remoteSettings, $serverProjectRoot); + // Remove this release on rollback. + $collection->rollback($this->removeFailedReleaseTask($serverRemoteConfig, $releaseDir)); + + // Clear opcache (if present) on rollback. + if (isset($remoteSettings['opcache']) && (!array_key_exists('clear', $remoteSettings['opcache']) || $remoteSettings['opcache']['clear'])) { + $collection->rollback($this->clearRemoteOpcacheTask($serverRemoteConfig)); + } + + // Push the package. + $collection->addTask($this->pushPackageTask($serverRemoteConfig, $archive)); + + // Add any tasks to execute before creating the symlinks. + $collection->addTask($this->preSymlinkTask($serverRemoteConfig)); + + // Switch the current symlink to the previous release on rollback. + $collection->rollback($this->remoteSwitchPreviousTask($serverRemoteConfig)); + + // Create the symlinks. + $collection->addTask($this->remoteSymlinksTask($serverRemoteConfig)); + + // Add any tasks to execute after creating the symlinks. + $collection->addTask($this->postSymlinkTask($serverRemoteConfig)); + } + + // Initialize the site (update or install). + $collection->addTask($this->initRemoteTask($workerRemoteConfig, $opts, $opts['force-install'])); + + // Clear cache after update or install. + $collection->addTask($this->clearCacheTask($workerRemoteConfig)); + + foreach ($servers as $server) { + $serverProjectRoot = $this->getCurrentProjectRoot($server, $user, $privateKeyFile, $remoteSettings); + $serverRemoteConfig = new RemoteConfig($server, $user, $privateKeyFile, $remoteSettings, $serverProjectRoot); + // Clear OPcache if present. + if (isset($remoteSettings['opcache']) && (!array_key_exists('clear', $remoteSettings['opcache']) || $remoteSettings['opcache']['clear'])) { + $collection->addTask($this->clearRemoteOpcacheTask($serverRemoteConfig)); + } + // Compress old releases if configured. + if (isset($remoteSettings['compress_old_releases']) && $remoteSettings['compress_old_releases']) { + $collection->addTask($this->compressOldReleaseTask($serverRemoteConfig)); + } + // Clean release and backup dirs on the servers. + $collection->completion($this->cleanDirsTask($serverRemoteConfig)); + } + + // Clear the site's cache on rollback too. + $collection->completion($this->clearCacheTask($workerRemoteConfig)); + + return $collection; + } + + /** + * Get the task that will create a release archive. + * + * @param string $archiveName + * The name of the archive that will be created. + * + * @return \Robo\Contract\TaskInterface + */ + protected function buildTask($archiveName) + { + return $this->handleTaskEvent('digipolis:build-task', ['archiveName' => $archiveName]); + } + + /** + * Get the task that will remove a failed release from the host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param string $releaseDir + * The release directory to remove. + * + * @return \Robo\Contract\TaskInterface + */ + protected function removeFailedReleaseTask(RemoteConfig $remoteConfig, $releaseDir) + { + return $this->handleTaskEvent( + 'digipolis:remove-failed-release', + [ + 'remoteConfig' => $remoteConfig, + 'releaseDir' => $releaseDir, + ] + ); + } + + /** + * Get the task that will clear opcache on a host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function clearRemoteOpcacheTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:clear-remote-opcache', + [ + 'remoteConfig' => $remoteConfig, + 'timeouts' => [ + 'clear_op_cache' => $this->getTimeoutSetting('clear_op_cache'), + ], + ] + ); + } + + /** + * Get the task that will push a release archive to a host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param string $archiveName + * The path to the archive to push. + * + * @return \Robo\Contract\TaskInterface + */ + protected function pushPackageTask(RemoteConfig $remoteConfig, $archiveName) + { + return $this->handleTaskEvent( + 'digipolis:push-package', + [ + 'remoteConfig' => $remoteConfig, + 'archiveName' => $archiveName, + ] + ); + } + + /** + * Get the task that will execute presymlink tasks. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function preSymlinkTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:pre-symlink', + [ + 'remoteConfig' => $remoteConfig, + 'timeouts' => [ + 'pre_symlink' => $this->getTimeoutSetting('pre_symlink'), + ], + ] + ); + } + + /** + * Get the task that will switch to the previous release on the host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function remoteSwitchPreviousTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:remote-switch-previous', + [ + 'remoteConfig' => $remoteConfig, + ] + ); + } + + /** + * Get the task that will create the configured symlinks on the host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return string + */ + protected function remoteSymlinksTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:remote-symlink', + [ + 'remoteConfig' => $remoteConfig, + 'timeouts' => [ + 'symlink' => $this->getTimeoutSetting('symlink'), + ], + ] + ); + } + + /** + * Get the task that will execute postsymlink tasks on the host + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function postSymlinkTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:post-symlink', + [ + 'remoteConfig' => $remoteConfig, + 'timeouts' => [ + 'post_symlink' => $this->getTimeoutSetting('post_symlink'), + ], + ] + ); + } + + /** + * Get the task that will install or update a site on the host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param array $options + * Extra parameters to pass to site install. + * @param bool $force + * Whether or not to force the install even when the site is present. + * + * @return \Robo\Contract\TaskInterface + * The init remote task. + */ + protected function initRemoteTask(RemoteConfig $remoteConfig, $options = [], $force = false) + { + $collection = $this->collectionBuilder(); + if (!$this->isSiteInstalled($remoteConfig) || $force) { + $this->say($force ? 'Forcing site install.' : 'Site status failed.'); + $this->say('Triggering install script.'); + + $collection->addTask($this->handleTaskEvent( + 'digipolis:install', + [ + 'remoteConfig' => $remoteConfig, + 'options'=> $options, + 'force' => $force, + ] + )); + + return $collection; + } + $collection->addTask($this->handleTaskEvent( + 'digipolis:update', + [ + 'remoteConfig' => $remoteConfig, + 'options'=> $options, + 'force' => $force, + ] + )); + + return $collection; + } + + /** + * Get the task that will compress an old release on the host. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return type + */ + protected function compressOldReleaseTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:compress-old-release', + [ + 'remoteConfig' => $remoteConfig, + 'releaseToCompress' => $remoteConfig->getCurrentProjectRoot(), + 'timeouts' => [ + 'compress_old_release' => $this->getTimeoutSetting('compress_old_release'), + ], + ] + ); + } + + /** + * Get the task that will clean the directories (remove old releases). + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * + * @return \Robo\Contract\TaskInterface + */ + protected function cleanDirsTask(RemoteConfig $remoteConfig) + { + return $this->handleTaskEvent( + 'digipolis:clean-dirs', + [ + 'remoteConfig' => $remoteConfig, + ] + ); + } +} diff --git a/src/Traits/DigipolisHelpersSyncCommandUtilities.php b/src/Traits/DigipolisHelpersSyncCommandUtilities.php new file mode 100644 index 0000000..9f8cf85 --- /dev/null +++ b/src/Traits/DigipolisHelpersSyncCommandUtilities.php @@ -0,0 +1,172 @@ + false, 'data' => false, 'rsync' => true] + ) { + if (!$opts['files'] && !$opts['data']) { + $opts['files'] = true; + $opts['data'] = true; + } + + $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync']; + + $sourceRemoteSettings = $this->getRemoteSettings( + $sourceHost, + $sourceUser, + $sourcePrivateKeyFile, + $sourceApp + ); + $sourceProjectRoot = $this->getCurrentProjectRoot($sourceHost, $sourceUser, $sourcePrivateKeyFile, $sourceRemoteSettings); + $sourceRemoteConfig = new RemoteConfig($sourceHost, $sourceUser, $sourcePrivateKeyFile, $sourceRemoteSettings, $sourceProjectRoot); + + $destinationRemoteSettings = $this->getRemoteSettings( + $destinationHost, + $destinationUser, + $destinationPrivateKeyFile, + $destinationApp + ); + $destinationProjectRoot = $this->getCurrentProjectRoot($destinationHost, $destinationUser, $destinationPrivateKeyFile, $destinationRemoteSettings); + $destinationRemoteConfig = new RemoteConfig($destinationHost, $destinationUser, $destinationPrivateKeyFile, $destinationRemoteSettings, $destinationProjectRoot); + + $collection = $this->collectionBuilder(); + + if ($opts['files'] && $opts['rsync']) { + // Files are rsync'ed, no need to sync them through backups later. + $opts['files'] = false; + $collection->addTask( + $this->rsyncFilesBetweenHostsTask( + $sourceRemoteConfig, + $destinationRemoteConfig, + ) + ); + } + + if ($opts['data'] || $opts['files']) { + // Create a backup on the source host. + $collection->addTask( + $this->backupRemoteTask($sourceRemoteConfig, $opts) + ); + // Download the backup from the source host to the local machine. + $collection->addTask( + $this->downloadBackupTask($sourceRemoteConfig, $opts) + ); + // Remove the backup from the source host. + $collection->addTask( + $this->removeBackupRemoteTask($sourceRemoteConfig, $opts) + ); + // Upload the backup to the destination host. + $collection->addTask( + $this->uploadBackupTask($destinationRemoteConfig, $opts) + ); + // Restore the backup on the destination host. + $collection->addTask( + $this->restoreBackupRemoteTask($destinationRemoteConfig, $opts) + ); + // Remove the backup from the destination host. + $collection->completion( + $this->removeBackupRemoteTask($destinationRemoteConfig, $opts) + ); + + // Finally remove the local backups. + $collection->completion($this->removeLocalBackupTask($sourceRemoteConfig, $opts)); + } + + $collection->completion($this->clearCacheTask($destinationRemoteConfig)); + + return $collection; + } + + /** + * Get the task that rsyncs files between hosts. + * + * @param RemoteConfig $sourceRemoteConfig + * RemoteConfig object populated with data relevant to the source. + * @param RemoteConfig $destinationRemoteConfig + * RemoteConfig object populated with data relevant to the destination. + * + * @return \Robo\Contract\TaskInterface + */ + protected function rsyncFilesBetweenHostsTask( + RemoteConfig $sourceRemoteConfig, + RemoteConfig $destinationRemoteConfig + ) { + return $this->handleTaskEvent( + 'digipolis:rsync-files-between-hosts', + [ + 'sourceRemoteConfig' => $sourceRemoteConfig, + 'destinationRemoteConfig' => $destinationRemoteConfig, + 'fileBackupConfig' => $this->getFileBackupConfig(), + 'timeouts' => [ + 'synctask_rsync' => $this->getTimeoutSetting('synctask_rsync') + ] + ] + ); + } + + /** + * Remove the local backup of a remote application. + * + * @param RemoteConfig $remoteConfig + * RemoteConfig object populated with data relevant to the host. + * @param array $options + * Options that were used to create the backup. + * + * @return \Robo\Contract\TaskInterface + */ + protected function removeLocalBackupTask(RemoteConfig $remoteConfig, $options) + { + return $this->handleTaskEvent( + 'digipolis:remove-local-backup', + [ + 'remoteConfig' => $remoteConfig, + 'fileBackupConfig' => $this->getFileBackupConfig(), + 'options' => $options, + ] + ); + } +} diff --git a/src/Traits/EventDispatcher.php b/src/Traits/EventDispatcher.php new file mode 100644 index 0000000..da47029 --- /dev/null +++ b/src/Traits/EventDispatcher.php @@ -0,0 +1,113 @@ +getEventHandlers($eventName); + + $event = new GenericEvent(); + $event->setArguments($arguments); + $collection = $this->collectionBuilder(); + foreach ($handlers as $handler) { + $collection->addTask($handler->handle($event)); + if ($event->isPropagationStopped()) { + break; + } + } + return $collection; + } + + /** + * Handle an event. + * + * @param string $eventName + * The name of the event to handle. + * + * @param array $arguments + * Associative array of arguments. + * + * @return array + * The results of the event handlers. + */ + protected function handleEvent(string $eventName, array $arguments): array + { + $handlers = $this->getEventHandlers($eventName); + + $event = new GenericEvent(); + $event->setArguments($arguments); + $result = []; + foreach ($handlers as $handler) { + $result[] = $handler->handle($event); + if ($event->isPropagationStopped()) { + break; + } + } + return $result; + } + + /** + * Returns a sorted (by priority) list of event handlers. + * + * @param string $eventName + * The name of the event to get the handlers for. + * + * @return EventHandlerWithPriority[] + * The sorted list of handlers for the given event. + */ + protected function getEventHandlers(string $eventName): array + { + $handlerFactories = $this->getCustomEventHandlers($eventName); + $handlers = []; + + foreach ($handlerFactories as $handlerFactory) { + /** @var EventHandlerWithPriority $handler */ + $handler = $handlerFactory(); + // If the handler implements the AddToContainerInterface, add it to + // the container, so all its dependencies are injected, based on the + // other interfaces it implements. + if ($handler instanceof AddToContainerInterface && $this instanceof ContainerAwareInterface) { + $class = get_class($handler); + if (!$this->getContainer()->has($class)) { + $this->getContainer()->addShared($class, $handler); + } + // Inflectors only run when getting the service. + $handler = $this->getContainer()->get($class); + } + + // Inject the builder if possible and needed. + if ($handler instanceof BuilderAwareInterface && $this instanceof BuilderAwareInterface && $this instanceof ContainerAwareInterface) { + $handler->setBuilder(CollectionBuilder::create($this->getContainer(), $handler)); + } + $handlers[] = $handler; + } + + usort($handlers, function (EventHandlerWithPriority $handlerA, EventHandlerWithPriority $handlerB) { + return $handlerA->getPriority() - $handlerB->getPriority(); + }); + + return $handlers; + } +} diff --git a/src/Traits/RemoteFilesBackupTrait.php b/src/Traits/RemoteFilesBackupTrait.php index e781bd9..89fcadc 100644 --- a/src/Traits/RemoteFilesBackupTrait.php +++ b/src/Traits/RemoteFilesBackupTrait.php @@ -2,7 +2,7 @@ namespace DigipolisGent\Robo\Helpers\Traits; -use DigipolisGent\Robo\Helpers\RemoteFilesBackup; +use DigipolisGent\Robo\Helpers\Robo\Plugin\Tasks\RemoteFilesBackup; use DigipolisGent\Robo\Task\Deploy\Ssh\Auth\AbstractAuth; trait RemoteFilesBackupTrait @@ -20,7 +20,7 @@ trait RemoteFilesBackupTrait * @param string $cwd * The working directory to execute the commands in. * - * @return \DigipolisGent\Robo\Helpers\RemoteFilesBackup + * @return \DigipolisGent\Robo\Helpers\Tasks\RemoteFilesBackup */ protected function taskRemoteFilesBackup($host, AbstractAuth $auth, $backupDir, $cwd) { diff --git a/src/Util/AddToContainerInterface.php b/src/Util/AddToContainerInterface.php new file mode 100644 index 0000000..78dd1b2 --- /dev/null +++ b/src/Util/AddToContainerInterface.php @@ -0,0 +1,7 @@ +host = $host; + $this->user = $user; + $this->privateKeyFile = $privateKeyFile; + $this->remoteSettings = $remoteSettings; + $this->currentProjectRoot = $currentProjectRoot; + } + + public function getHost(): string { + return $this->host; + } + + public function getUser(): string + { + return $this->user; + } + + public function getPrivateKeyFile(): string + { + return $this->privateKeyFile; + } + + public function getRemoteSettings(): array + { + return $this->remoteSettings; + } + + public function getCurrentProjectRoot(): string + { + return $this->currentProjectRoot; + } + + public function setHost(string $host): void + { + $this->host = $host; + } + + public function setUser(string $user): void + { + $this->user = $user; + } + + public function setPrivateKeyFile(string $privateKeyFile): void + { + $this->privateKeyFile = $privateKeyFile; + } + + public function setRemoteSettings(array $remoteSettings): void + { + $this->remoteSettings = $remoteSettings; + } + + public function setCurrentProjectRoot(string $currentProjectRoot): void + { + $this->currentProjectRoot = $currentProjectRoot; + } +} diff --git a/src/Util/RemoteHelper.php b/src/Util/RemoteHelper.php deleted file mode 100644 index 1dc91bc..0000000 --- a/src/Util/RemoteHelper.php +++ /dev/null @@ -1,333 +0,0 @@ - 'HOSTNAME', - 'environment_matcher' => '\\DigipolisGent\\Robo\\Helpers\\Util\\EnvironmentMatcher::regexMatch', - ]; - - protected $time; - - protected $projectRoots = []; - - public function __construct(int $time, ConfigInterface $config, PropertiesHelper $propertiesHelper) - { - $this->time = $time; - $this->setConfig($config); - $this->setPropertiesHelper($propertiesHelper); - } - - public static function create(DefinitionContainerInterface $container) - { - $object = new static( - $container->get('digipolis.time'), - $container->get('config'), - $container->get(PropertiesHelper::class) - ); - $object->setBuilder(CollectionBuilder::create($container, $object)); - - return $object; - } - - public function getTime() - { - return $this->time; - } - - - /** - * Get the settings from the 'remote' config key, with the tokens replaced. - * - * @param string $host - * The IP address of the server to get the settings for. - * @param string $user - * The SSH user used to connect to the server. - * @param string $keyFile - * The path to the private key file used to connect to the server. - * @param string $app - * The name of the app these settings apply to. - * @param string|null $timestamp - * The timestamp to use. Defaults to the request time. - * - * @return array - * The settings for this server and app. - */ - public function getRemoteSettings($host, $user, $keyFile, $app, $timestamp = null) - { - $this->propertiesHelper->readProperties(); - $defaults = [ - 'user' => $user, - 'private-key' => $keyFile, - 'app' => $app, - 'createbackup' => true, - 'time' => is_null($timestamp) ? $this->time : $timestamp, - ]; - - // Set up destination config. - $replacements = array( - '[user]' => $user, - '[private-key]' => $keyFile, - '[app]' => $app, - '[time]' => is_null($timestamp) ? $this->time : $timestamp, - ); - if (is_string($host)) { - $replacements['[server]'] = $host; - $defaults['server'] = $host; - } - if (is_array($host)) { - foreach ($host as $key => $server) { - $replacements['[server-' . $key . ']'] = $server; - $defaults['server-' . $key] = $server; - } - } - - $settings = $this->processEnvironmentOverrides( - $this->tokenReplace($this->getConfig()->get('remote'), $replacements) + $defaults - ); - - // Reverse the symlinks so the `current` symlink is the last one to be - // created. - $settings['symlinks'] = array_reverse($settings['symlinks'], true); - - return $settings; - } - - /** - * Get the settings from the 'local' config key, with the tokens replaced. - * - * @param string $app - * The name of the app these settings apply to. - * @param string|null $timestamp - * The timestamp to use. Defaults to the request time. - * - * @return array - * The settings for the local environment and app. - */ - public function getLocalSettings($app = null, $timestamp = null) - { - $this->propertiesHelper->readProperties(); - $defaults = [ - 'app' => $app, - 'time' => is_null($timestamp) ? $this->time : $timestamp, - 'project_root' => $this->getConfig()->get('digipolis.root.project'), - 'web_root' => $this->getConfig()->get('digipolis.root.web'), - ]; - - // Set up destination config. - $replacements = array( - '[project_root]' => $this->getConfig()->get('digipolis.root.project'), - '[web_root]' => $this->getConfig()->get('digipolis.root.web'), - '[app]' => $app, - '[time]' => is_null($timestamp) ? $this->time : $timestamp, - ); - return $this->tokenReplace($this->getConfig()->get('local'), $replacements) + $defaults; - } - - - /** - * Process environment-specific overrides. - * - * @param array $settings - * @return array - * - * @see self::getRemoteSettings - */ - protected function processEnvironmentOverrides($settings) - { - $settings += static::$defaultEnvironmentOverrideSettings; - if (!isset($settings['environment_overrides']) || !$settings['environment_overrides']) { - return $settings; - } - - $server = $this->getFirstServer($settings); - if (!$server) { - return $settings; - } - - // Parse the env var on the server. - $auth = new KeyFile($settings['user'], $settings['private-key']); - $fullOutput = ''; - $this->taskSsh($server, $auth) - ->exec( - (string) CommandBuilder::create('echo') - ->addRawArgument('$' . $settings['environment_env_var']), - function ($output) use (&$fullOutput) { - $fullOutput .= $output; - } - ) - ->run(); - $envVarValue = substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput))); - foreach ($settings['environment_overrides'] as $environmentMatch => $overrides) { - if (call_user_func($settings['environment_matcher'], $environmentMatch, $envVarValue)) { - $settings = ArrayMerger::doMerge($settings, $overrides); - } - } - return $settings; - } - - /** - * Get the first server entry from the remote settings. - * - * @param array $settings - * - * @return string|bool - * First server if found, false otherwise. - * - * @see self::processEnvironmentOverrides - */ - protected function getFirstServer($settings) - { - foreach ($settings as $key => $value) { - if (preg_match('/^server/', $key) === 1) { - return $value; - } - } - return false; - } - - /** - * Helper functions to replace tokens in an array. - * - * @param string|array $input - * The array or string containing the tokens to replace. - * @param array $replacements - * The token replacements. - * - * @return string|array - * The input with the tokens replaced with their values. - */ - protected function tokenReplace($input, $replacements) - { - if (is_string($input)) { - return strtr($input, $replacements); - } - if (is_scalar($input) || empty($input)) { - return $input; - } - foreach ($input as &$i) { - $i = $this->tokenReplace($i, $replacements); - } - return $input; - } - - public function getCurrentProjectRoot($worker, AbstractAuth $auth, $remote) - { - $key = $worker . ':' . $auth->getUser() . ':' . $remote['releasesdir']; - if (!array_key_exists($key, $this->projectRoots)) { - $fullOutput = ''; - $this->taskSsh($worker, $auth) - ->remoteDirectory($remote['releasesdir'], true) - ->exec( - (string) CommandBuilder::create('ls') - ->addFlag('1') - ->pipeOutputTo( - CommandBuilder::create('sort') - ->addFlag('r') - ->pipeOutputTo( - CommandBuilder::create('head') - ->addFlag('1') - ) - ), - function ($output) use (&$fullOutput) { - $fullOutput .= $output; - } - ) - ->run(); - $this->projectRoots[$key] = $remote['releasesdir'] . '/' . substr($fullOutput, 0, (strpos($fullOutput, "\n") ?: strlen($fullOutput))); - } - return $this->projectRoots[$key]; - } - - /** - * Check if the current release has robo available. - * - * @param string $worker - * The server to check the release on. - * @param \DigipolisGent\Robo\Helpers\Traits\AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return bool - */ - public function currentReleaseHasRobo($worker, AbstractAuth $auth, $remote) - { - $currentProjectRoot = $this->getCurrentProjectRoot($worker, $auth, $remote); - return $this->taskSsh($worker, $auth) - ->remoteDirectory($currentProjectRoot, true) - ->exec( - (string) CommandBuilder::create('ls') - ->addArgument('vendor/bin/robo') - ->pipeOutputTo( - CommandBuilder::create('grep') - ->addArgument('robo') - ) - ) - ->run() - ->wasSuccessful(); - } - - /** - * Timeouts can be overwritten in properties.yml under the `timeout` key. - * - * @param string $setting - * - * @return int - */ - public function getTimeoutSetting($setting) - { - $timeoutSettings = $this->getTimeoutSettings(); - return isset($timeoutSettings[$setting]) ? $timeoutSettings[$setting] : static::DEFAULT_TIMEOUT; - } - - protected function getTimeoutSettings() - { - $this->propertiesHelper->readProperties(); - return $this->getConfig()->get('timeouts', []) + $this->getDefaultTimeoutSettings(); - } - - protected function getDefaultTimeoutSettings() - { - // Refactor this to default.properties.yml - return [ - 'presymlink_mirror_dir' => 60, - 'synctask_rsync' => 1800, - 'backup_files' => 300, - 'backup_database' => 300, - 'remove_backup' => 300, - 'restore_files_backup' => 300, - 'restore_db_backup' => 60, - 'pre_restore_remove_files' => 300, - 'clean_dir' => 30, - 'clear_op_cache' => 30, - 'compress_old_release' => 300, - ]; - } -} diff --git a/src/Util/TaskFactory/AbstractApp.php b/src/Util/TaskFactory/AbstractApp.php deleted file mode 100644 index ef07063..0000000 --- a/src/Util/TaskFactory/AbstractApp.php +++ /dev/null @@ -1,97 +0,0 @@ -setConfig($config); - } - - public static function create(DefinitionContainerInterface $container) - { - $object = new static($container->get('config')); - $object->setBuilder(CollectionBuilder::create($container, $object)); - - return $object; - } - - /** - * Install the site in the current folder. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * @param bool $force - * Whether or not to force the install even when the site is present. - * - * @return \Robo\Contract\TaskInterface - * The install task. - */ - abstract public function installTask($worker, AbstractAuth $auth, $remote, $extra = [], $force = false); - - /** - * Executes database updates of the site in the current folder. - * - * Executes database updates of the site in the current folder. Sets - * the site in maintenance mode before the update and takes in out of - * maintenance mode after. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The update task. - */ - abstract public function updateTask($worker, AbstractAuth $auth, $remote); - - /** - * Check if a site is already installed - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return bool - * Whether or not the site is installed. - */ - abstract public function isSiteInstalled($worker, AbstractAuth $auth, $remote); - - /** - * Clear cache of the site. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return bool|\Robo\Contract\TaskInterface - * The clear cache task or false if no clear cache task exists. - */ - abstract public function clearCacheTask($worker, $auth, $remote); -} diff --git a/src/Util/TaskFactory/Backup.php b/src/Util/TaskFactory/Backup.php deleted file mode 100644 index c113fd1..0000000 --- a/src/Util/TaskFactory/Backup.php +++ /dev/null @@ -1,334 +0,0 @@ -setRemoteHelper($remoteHelper); - } - - public static function create(DefinitionContainerInterface $container) - { - $object = new static( - $container->get(RemoteHelper::class) - ); - $object->setBuilder(CollectionBuilder::create($container, $object)); - - return $object; - } - - /** - * Create a backup of files (storage folder) and database. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The backup task. - */ - public function backupTask( - $worker, - AbstractAuth $auth, - $remote, - $opts = ['files' => false, 'data' => false] - ) { - if (!$opts['files'] && !$opts['data']) { - $opts['files'] = true; - $opts['data'] = true; - } - $backupConfig = $this->getBackupConfig(); - $backupDir = $remote['backupsdir'] . '/' . $remote['time']; - $collection = $this->collectionBuilder(); - if ($opts['files']) { - $collection - ->taskRemoteFilesBackup($worker, $auth, $backupDir, $remote['filesdir']) - ->backupFile($this->backupFileName('.tar.gz')) - ->excludeFromBackup($backupConfig['exclude_from_backup']) - ->backupSubDirs($backupConfig['file_backup_subdirs']) - ->timeout($this->remoteHelper->getTimeoutSetting('backup_files')); - } - - if ($opts['data']) { - $currentProjectRoot = $this->remoteHelper->getCurrentProjectRoot($worker, $auth, $remote); - $collection - ->taskRemoteDatabaseBackup($worker, $auth, $backupDir, $currentProjectRoot) - ->backupFile($this->backupFileName('.sql')) - ->timeout($this->remoteHelper->getTimeoutSetting('backup_database')); - } - return $collection; - } - - /** - * Restore a backup of files (storage folder) and database. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The restore backup task. - */ - public function restoreBackupTask( - $worker, - AbstractAuth $auth, - $remote, - $opts = ['files' => false, 'data' => false] - ) { - if (!$opts['files'] && !$opts['data']) { - $opts['files'] = true; - $opts['data'] = true; - } - - $currentProjectRoot = $this->remoteHelper->getCurrentProjectRoot($worker, $auth, $remote); - $backupDir = $remote['backupsdir'] . '/' . $remote['time']; - - $collection = $this->collectionBuilder(); - - // Restore the files backup. - $preRestoreBackup = $this->preRestoreBackupTask($worker, $auth, $remote, $opts); - if ($preRestoreBackup) { - $collection->addTask($preRestoreBackup); - } - - if ($opts['files']) { - $filesBackupFile = $this->backupFileName('.tar.gz', $remote['time']); - $collection - ->taskSsh($worker, $auth) - ->remoteDirectory($remote['filesdir'], true) - ->timeout($this->remoteHelper->getTimeoutSetting('restore_files_backup')) - ->exec( - (string) CommandBuilder::create('tar') - ->addFlag('xkz') - ->addFlag('f', $backupDir . '/' . $filesBackupFile) - ); - } - - // Restore the db backup. - if ($opts['data']) { - $dbBackupFile = $this->backupFileName('.sql.gz', $remote['time']); - $collection - ->taskSsh($worker, $auth) - ->remoteDirectory($currentProjectRoot, true) - ->timeout($this->remoteHelper->getTimeoutSetting('restore_db_backup')) - ->exec( - (string) CommandBuilder::create('vendor/bin/robo digipolis:database-restore') - ->addOption('source', $backupDir . '/' . $dbBackupFile) - ); - } - return $collection; - } - - - /** - * Pre restore backup task. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return bool|\Robo\Contract\TaskInterface - * The pre restore backup task, false if no pre restore backup tasks need - * to run. - */ - protected function preRestoreBackupTask( - $worker, - AbstractAuth $auth, - $remote, - $opts = ['files' => false, 'data' => false] - ) { - if (!$opts['files'] && !$opts['data']) { - $opts['files'] = true; - $opts['data'] = true; - } - if ($opts['files']) { - $backupConfig = $this->getBackupConfig(); - $removeFiles = CommandBuilder::create('rm')->addFlag('rf'); - if (!$backupConfig['file_backup_subdirs']) { - $removeFiles->addArgument('./*'); - $removeFiles->addArgument('./.??*'); - } - foreach ($backupConfig['file_backup_subdirs'] as $subdir) { - $removeFiles->addArgument($subdir . '/*'); - $removeFiles->addArgument($subdir . '/.??*'); - } - - return $this->taskSsh($worker, $auth) - ->remoteDirectory($remote['filesdir'], true) - // Files dir can be pretty big on large sites. - ->timeout($this->remoteHelper->getTimeoutSetting('pre_restore_remove_files')) - ->exec((string) $removeFiles); - } - - return false; - } - - /** - * Remove a backup. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The backup task. - */ - public function removeBackupTask( - $worker, - AbstractAuth $auth, - $remote, - $opts = ['files' => false, 'data' => false] - ) { - $backupDir = $remote['backupsdir'] . '/' . $remote['time']; - - $collection = $this->collectionBuilder(); - $collection->taskSsh($worker, $auth) - ->timeout($this->remoteHelper->getTimeoutSetting('remove_backup')) - ->exec( - (string) CommandBuilder::create('rm') - ->addFlag('rf') - ->addArgument($backupDir) - ); - - return $collection; - } - - /** - * Download a backup of files (storage folder) and database. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The download backup task. - */ - public function downloadBackupTask( - $worker, - AbstractAuth $auth, - $remote, - $opts = ['files' => false, 'data' => false] - ) { - if (!$opts['files'] && !$opts['data']) { - $opts['files'] = true; - $opts['data'] = true; - } - $backupDir = $remote['backupsdir'] . '/' . $remote['time']; - - $collection = $this->collectionBuilder(); - $collection - ->taskSFTP($worker, $auth); - - // Download files. - if ($opts['files']) { - $filesBackupFile = $this->backupFileName('.tar.gz', $remote['time']); - $collection->get($backupDir . '/' . $filesBackupFile, $filesBackupFile); - } - - // Download data. - if ($opts['data']) { - $dbBackupFile = $this->backupFileName('.sql.gz', $remote['time']); - $collection->get($backupDir . '/' . $dbBackupFile, $dbBackupFile); - } - return $collection; - } - - /** - * Upload a backup of files (storage folder) and database to a server. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The upload backup task. - */ - public function uploadBackupTask( - $worker, - AbstractAuth $auth, - $remote, - $opts = ['files' => false, 'data' => false] - ) { - if (!$opts['files'] && !$opts['data']) { - $opts['files'] = true; - $opts['data'] = true; - } - $backupDir = $remote['backupsdir'] . '/' . $remote['time']; - $dbBackupFile = $this->backupFileName('.sql.gz', $remote['time']); - $filesBackupFile = $this->backupFileName('.tar.gz', $remote['time']); - - $collection = $this->collectionBuilder(); - $collection - ->taskSsh($worker, $auth) - ->exec((string) CommandBuilder::create('mkdir')->addFlag('p')->addArgument($backupDir)) - ->taskSFTP($worker, $auth); - if ($opts['files']) { - $collection->put($backupDir . '/' . $filesBackupFile, $filesBackupFile); - } - if ($opts['data']) { - $collection->put($backupDir . '/' . $dbBackupFile, $dbBackupFile); - } - return $collection; - } - - /** - * Generate a backup filename based on the given time. - * - * @param string $extension - * The extension to append to the filename. Must include leading dot. - * @param int|null $timestamp - * The timestamp to generate the backup name from. Defaults to the request - * time. - * - * @return string - * The generated filename. - */ - public function backupFileName($extension, $timestamp = null) - { - if (is_null($timestamp)) { - $timestamp = $this->remoteHelper->getTime(); - } - return $timestamp . '_' . date('Y_m_d_H_i_s', $timestamp) . $extension; - } -} diff --git a/src/Util/TaskFactory/BackupConfigTrait.php b/src/Util/TaskFactory/BackupConfigTrait.php deleted file mode 100644 index c45fedf..0000000 --- a/src/Util/TaskFactory/BackupConfigTrait.php +++ /dev/null @@ -1,26 +0,0 @@ -getCustomEventHandlers('digipolis-backup-config'); - $backupConfig = [ - 'file_backup_subdirs' => [], - 'exclude_from_backup' => [], - ]; - foreach ($handlers as $handler) { - $handlerConfig = $handler(); - if (isset($handlerConfig['file_backup_subdirs'])) { - $backupConfig['file_backup_subdirs'] = array_merge($backupConfig['file_backup_subdirs'], $handlerConfig['file_backup_subdirs']); - } - - if (isset($handlerConfig['exclude_from_backup'])) { - $backupConfig['exclude_from_backup'] = array_merge($backupConfig['exclude_from_backup'], $handlerConfig['exclude_from_backup']); - } - } - } - -} diff --git a/src/Util/TaskFactory/Build.php b/src/Util/TaskFactory/Build.php deleted file mode 100644 index 3f3201b..0000000 --- a/src/Util/TaskFactory/Build.php +++ /dev/null @@ -1,58 +0,0 @@ -setPropertiesHelper($propertiesHelper); - $this->setRemoteHelper($remoteHelper); - } - - public static function create(DefinitionContainerInterface $container) - { - $object = new static( - $container->get(PropertiesHelper::class), - $container->get(RemoteHelper::class) - ); - $object->setBuilder(CollectionBuilder::create($container, $object)); - - return $object; - } - - /** - * Build a site and package it. - * - * @param string $archivename - * Name of the archive to create. - * - * @return \Robo\Contract\TaskInterface - * The deploy task. - */ - public function buildTask($archivename = null) - { - $this->propertiesHelper->readProperties(); - $archive = is_null($archivename) ? $this->remoteHelper->getTime() . '.tar.gz' : $archivename; - $collection = $this->collectionBuilder(); - $collection - ->taskPackageProject($archive); - return $collection; - } -} diff --git a/src/Util/TaskFactory/Cache.php b/src/Util/TaskFactory/Cache.php deleted file mode 100644 index de2ea86..0000000 --- a/src/Util/TaskFactory/Cache.php +++ /dev/null @@ -1,83 +0,0 @@ -setAppTaskFactory($appTaskFactory); - $this->setRemoteHelper($remoteHelper); - } - - public static function create(DefinitionContainerInterface $container) - { - $object = new static( - $container->get(AbstractApp::class), - $container->get(RemoteHelper::class) - ); - $object->setBuilder(CollectionBuilder::create($container, $object)); - - return $object; - } - - /** - * Clear cache of the site. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return bool|\Robo\Contract\TaskInterface - * The clear cache task or false if no clear cache task exists. - */ - public function clearCacheTask($worker, $auth, $remote) - { - return $this->appTaskFactory->clearCacheTask($worker, $auth, $remote); - } - - /** - * Clear OPcache on the server. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The clear OPcache task. - */ - public function clearOpCacheTask($worker, AbstractAuth $auth, $remote) - { - $clearOpcache = CommandBuilder::create('vendor/bin/robo digipolis:clear-op-cache')->addArgument($remote['opcache']['env']); - if (isset($remote['opcache']['host'])) { - $clearOpcache->addOption('host', $remote['opcache']['host']); - } - return $this->taskSsh($worker, $auth) - ->remoteDirectory($remote['rootdir'], true) - ->timeout($this->remoteHelper->getTimeoutSetting('clear_op_cache')) - ->exec((string) $clearOpcache); - } -} diff --git a/src/Util/TaskFactory/Deploy.php b/src/Util/TaskFactory/Deploy.php deleted file mode 100644 index 1369037..0000000 --- a/src/Util/TaskFactory/Deploy.php +++ /dev/null @@ -1,539 +0,0 @@ -setAppTaskFactory($appTaskFactory); - $this->setBackupTaskFactory($backupTaskFactory); - $this->setBuildTaskFactory($buildTaskFactory); - $this->setCacheTaskFactory($cacheTaskFactory); - $this->setPropertiesHelper($propertiesHelper); - $this->setRemoteHelper($remoteHelper); - } - - public static function create(DefinitionContainerInterface $container) - { - $object = new static( - $container->get(AbstractApp::class), - $container->get(Backup::class), - $container->get(Build::class), - $container->get(Cache::class), - $container->get(PropertiesHelper::class), - $container->get(RemoteHelper::class) - ); - $object->setBuilder(CollectionBuilder::create($container, $object)); - - return $object; - } - - /** - * Build a site and push it to the servers. - * - * @param array $arguments - * Variable amount of arguments. The last argument is the path to the - * the private key file (ssh), the penultimate is the ssh user. All - * arguments before that are server IP's to deploy to. - * @param array $opts - * The options for this task. - * - * @return \Robo\Contract\TaskInterface - * The deploy task. - */ - public function deployTask( - array $arguments, - $opts - ) { - // Define variables. - $opts += ['force-install' => false]; - $privateKeyFile = array_pop($arguments); - $user = array_pop($arguments); - $servers = $arguments; - $worker = is_null($opts['worker']) ? reset($servers) : $opts['worker']; - $remote = $this->remoteHelper->getRemoteSettings($servers, $user, $privateKeyFile, $opts['app']); - $releaseDir = $remote['releasesdir'] . '/' . $remote['time']; - $auth = new KeyFile($user, $privateKeyFile); - $archive = $remote['time'] . '.tar.gz'; - $backupOpts = ['files' => false, 'data' => true]; - - $collection = $this->collectionBuilder(); - - // Build the archive to deploy. - $collection->addTask($this->buildTaskFactory->buildTask($archive)); - - // Create a backup and a rollback task if a site is already installed. - if ($remote['createbackup'] && $this->appTaskFactory->isSiteInstalled($worker, $auth, $remote) && $this->remoteHelper->currentReleaseHasRobo($worker, $auth, $remote)) { - // Create a backup. - $collection->addTask($this->backupTaskFactory->backupTask($worker, $auth, $remote, $backupOpts)); - - // Create a rollback for this backup for when the deploy fails. - $collection->rollback( - $this->backupTaskFactory->restoreBackupTask( - $worker, - $auth, - $remote, - $backupOpts - ) - ); - } - - // Push the package to the servers and create the required symlinks. - foreach ($servers as $server) { - // Remove this release on rollback. - $collection->rollback($this->removeFailedReleaseTask($server, $auth, $remote, $releaseDir)); - - // Clear opcache (if present) on rollback. - if (isset($remote['opcache']) && (!array_key_exists('clear', $remote['opcache']) || $remote['opcache']['clear'])) { - $collection->rollback($this->cacheTaskFactory->clearOpCacheTask($server, $auth, $remote)); - } - - // Push the package. - $collection->addTask($this->pushPackageTask($server, $auth, $remote, $archive)); - - // Add any tasks to execute before creating the symlinks. - $preSymlink = $this->preSymlinkTask($server, $auth, $remote); - if ($preSymlink) { - $collection->addTask($preSymlink); - } - - // Switch the current symlink to the previous release on rollback. - $collection->rollback($this->switchPreviousTask($server, $auth, $remote)); - - // Create the symlinks. - $collection->addTask($this->symlinksTask($server, $auth, $remote)); - $postSymlink = $this->postSymlinkTask($server, $auth, $remote); - if ($postSymlink) { - $collection->addTask($postSymlink); - } - } - - // Initialize the site (update or install). - $collection->addTask($this->initRemoteTask($worker, $auth, $remote, $opts, $opts['force-install'])); - - // Clear cache after update or install. - $clearCache = $this->cacheTaskFactory->clearCacheTask($worker, $auth, $remote); - if ($clearCache) { - $collection->addTask($clearCache); - } - - // Clear OPcache if present. - if (isset($remote['opcache']) && (!array_key_exists('clear', $remote['opcache']) || $remote['opcache']['clear'])) { - foreach ($servers as $server) { - $collection->addTask($this->cacheTaskFactory->clearOpCacheTask($server, $auth, $remote)); - } - } - - foreach ($servers as $server) { - // Compress old releases if configured. - if (isset($remote['compress_old_releases']) && $remote['compress_old_releases']) { - // The current release (the one we're replacing). - $currentRelease = $this->remoteHelper->getCurrentProjectRoot($server, $auth, $remote); - // Strip the releases dir from the current release, so the tar - // contains relative paths. - $relativeCurrentRelease = str_replace($remote['releasesdir'] . '/', '', $currentRelease); - $collection->addTask( - $this->taskSsh($server, $auth) - ->remoteDirectory($remote['releasesdir']) - ->exec((string) CommandBuilder::create('tar') - ->addFlag('c') - ->addFlag('z') - ->addFlag('f', $relativeCurrentRelease . '.tar.gz') - ->addArgument($relativeCurrentRelease) - ->onSuccess( - CommandBuilder::create('chown') - ->addFlag('R') - ->addArgument($auth->getUser() . ':' . $auth->getUser()) - ->addArgument($relativeCurrentRelease) - ->onSuccess(CommandBuilder::create('chmod') - ->addFlag('R') - ->addArgument('a+rwx') - ->addArgument($relativeCurrentRelease) - ->onSuccess(CommandBuilder::create('rm') - ->addFlag('rf') - ->addArgument($relativeCurrentRelease) - ) - ) - ) - ->onFailure( - CommandBuilder::create('rm') - ->addFlag('r') - ->addFlag('f') - ->addArgument($relativeCurrentRelease . '.tar.gz') - ) - )->timeout($this->remoteHelper->getTimeoutSetting('compress_old_release')) - ); - } - // Clean release and backup dirs on the servers. - $collection->completion($this->cleanDirsTask($server, $auth, $remote)); - } - - // Clear the site's cache on rollback too. - if ($clearCache) { - $collection->completion($clearCache); - } - - return $collection; - } - - /** - * Tasks to execute before creating the symlinks. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return bool|\Robo\Contract\TaskInterface - * The presymlink task, false if no pre symlink tasks need to run. - */ - protected function preSymlinkTask($worker, AbstractAuth $auth, $remote) - { - $collection = $this->collectionBuilder(); - foreach ($remote['symlinks'] as $symlink) { - $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($worker, $auth, $remote, $symlink); - if ($preIndividualSymlinkTask) { - $collection->addTask($preIndividualSymlinkTask); - } - } - return $collection; - } - - /** - * Tasks to execute before creating an individual symlink. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * @param string $symlink - * The symlink in format "target:link". - * - * @return bool|\Robo\Contract\TaskInterface - * The presymlink task, false if no pre symlink task needs to run. - */ - protected function preIndividualSymlinkTask($worker, AbstractAuth $auth, $remote, $symlink) - { - $projectRoot = $remote['rootdir']; - $collection = $this->collectionBuilder(); - $collection->taskSsh($worker, $auth) - ->remoteDirectory($projectRoot, true) - ->timeout($this->remoteHelper->getTimeoutSetting('presymlink_mirror_dir')); - list($target, $link) = explode(':', $symlink); - if ($link === $remote['currentdir']) { - return; - } - // If the link we're going to create is an existing directory, - // mirror that directory on the symlink target and then delete it - // before creating the symlink - $collection->exec( - (string) CommandBuilder::create('vendor/bin/robo digipolis:mirror-dir') - ->addArgument($link) - ->addArgument($target) - ); - $collection->exec( - (string) CommandBuilder::create('rm') - ->addFlag('rf') - ->addArgument($link) - ); - - return $collection; - } - - - - /** - * Create all required symlinks on the server. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The symlink task. - */ - protected function symlinksTask($worker, AbstractAuth $auth, $remote) - { - $collection = $this->collectionBuilder(); - foreach ($remote['symlinks'] as $link) { - $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($worker, $auth, $remote, $link); - if ($preIndividualSymlinkTask) { - $collection->addTask($preIndividualSymlinkTask); - } - list($target, $linkname) = explode(':', $link); - $collection->taskSsh($worker, $auth) - ->exec( - (string) CommandBuilder::create('ln') - ->addFlag('s') - ->addFlag('T') - ->addFlag('f') - ->addArgument($target) - ->addArgument($linkname) - ); - } - return $collection; - } - - /** - * Tasks to execute after creating the symlinks. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return bool|\Robo\Contract\TaskInterface - * The postsymlink task, false if no post symlink tasks need to run. - */ - protected function postSymlinkTask($worker, AbstractAuth $auth, $remote) - { - if (isset($remote['postsymlink_filechecks']) && $remote['postsymlink_filechecks']) { - $projectRoot = $remote['rootdir']; - $collection = $this->collectionBuilder(); - $collection->taskSsh($worker, $auth) - ->remoteDirectory($projectRoot, true) - ->timeout($this->remoteHelper->getTimeoutSetting('postsymlink_filechecks')); - foreach ($remote['postsymlink_filechecks'] as $file) { - // If this command fails, the collection will fail, which will - // trigger a rollback. - $builder = CommandBuilder::create('ls') - ->addArgument($file) - ->pipeOutputTo('grep') - ->addArgument($file) - ->onFailure( - CommandBuilder::create('echo') - ->addArgument('[ERROR] ' . $file . ' was not found.') - ->onFinished('exit') - ->addArgument('1') - ); - $collection->exec((string) $builder); - } - return $collection; - } - return false; - } - - /** - * Install or update a remote site. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * @param array $extra - * Extra parameters to pass to site install. - * @param bool $force - * Whether or not to force the install even when the site is present. - * - * @return \Robo\Contract\TaskInterface - * The init remote task. - */ - protected function initRemoteTask($worker, AbstractAuth $auth, $remote, $extra = [], $force = false) - { - $collection = $this->collectionBuilder(); - if (!$this->appTaskFactory->isSiteInstalled($worker, $auth, $remote) || $force) { - $this->say($force ? 'Forcing site install.' : 'Site status failed.'); - $this->say('Triggering install script.'); - - $collection->addTask($this->appTaskFactory->installTask($worker, $auth, $remote, $extra, $force)); - return $collection; - } - $collection->addTask($this->appTaskFactory->updateTask($worker, $auth, $remote, $extra)); - return $collection; - } - - /** - * Build a site and package it. - * - * @param string $archivename - * Name of the archive to create. - * - * @return \Robo\Contract\TaskInterface - * The deploy task. - */ - protected function buildTask($archivename = null) - { - $this->propertiesHelper->readProperties(); - $archive = is_null($archivename) ? $this->time . '.tar.gz' : $archivename; - $collection = $this->collectionBuilder(); - $collection - ->taskPackageProject($archive); - return $collection; - } - - /** - * Remove a failed release from the server. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * @param string|null $releaseDirname - * The path of the release dir to remove. - * - * @return \Robo\Contract\TaskInterface - * The remove release task. - */ - protected function removeFailedReleaseTask($worker, AbstractAuth $auth, $remote, $releaseDirname = null) - { - $releaseDir = is_null($releaseDirname) - ? $remote['releasesdir'] . '/' . $remote['time'] - : $releaseDirname; - return $this->taskRemoteRemoveRelease($worker, $auth, null, $releaseDir); - } - - - - /** - * Push a package to the server. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * @param string|null $archivename - * The path to the package to push. - * - * @return \Robo\Contract\TaskInterface - * The push package task. - */ - protected function pushPackageTask($worker, AbstractAuth $auth, $remote, $archivename = null) - { - $archive = is_null($archivename) - ? $remote['time'] . '.tar.gz' - : $archivename; - $releaseDir = $remote['releasesdir'] . '/' . $remote['time']; - $collection = $this->collectionBuilder(); - $collection->taskPushPackage($worker, $auth) - ->destinationFolder($releaseDir) - ->package($archive); - - $collection->taskSsh($worker, $auth) - ->remoteDirectory($releaseDir, true) - ->exec((string) CommandBuilder::create('chmod') - ->addArgument('u+rx') - ->addArgument('vendor/bin/robo') - ); - - return $collection; - } - - /** - * Switch the current symlink to the previous release on the server. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The switch previous task. - */ - protected function switchPreviousTask($worker, AbstractAuth $auth, $remote) - { - return $this->taskRemoteSwitchPrevious( - $worker, - $auth, - $this->remoteHelper->getCurrentProjectRoot($worker, $auth, $remote), - $remote['releasesdir'], - $remote['currentdir'] - ); - } - - /** - * Clean the release and backup directories on the server. - * - * @param string $worker - * The server to install the site on. - * @param AbstractAuth $auth - * The ssh authentication to connect to the server. - * @param array $remote - * The remote settings for this server. - * - * @return \Robo\Contract\TaskInterface - * The clean directories task. - */ - protected function cleanDirsTask($worker, AbstractAuth $auth, $remote) - { - $cleandirLimit = isset($remote['cleandir_limit']) ? max(1, $remote['cleandir_limit']) : ''; - $collection = $this->collectionBuilder(); - $collection->taskRemoteCleanDirs($worker, $auth, $remote['rootdir'], $remote['releasesdir'], ($cleandirLimit ? ($cleandirLimit + 1) : false)); - - if ($remote['createbackup']) { - $collection->taskRemoteCleanDirs($worker, $auth, $remote['rootdir'], $remote['backupsdir'], ($cleandirLimit ? ($cleandirLimit) : false)); - } - - return $collection; - } - -} diff --git a/src/Util/TaskFactory/Sync.php b/src/Util/TaskFactory/Sync.php deleted file mode 100644 index 1bdc73f..0000000 --- a/src/Util/TaskFactory/Sync.php +++ /dev/null @@ -1,412 +0,0 @@ -setBackupTaskFactory($backupTaskFactory); - $this->setBuildTaskFactory($buildTaskFactory); - $this->setCacheTaskFactory($cacheTaskFactory); - $this->setRemoteHelper($remoteHelper); - } - - public static function create(DefinitionContainerInterface $container) - { - $object = new static( - $container->get(Backup::class), - $container->get(Build::class), - $container->get(Cache::class), - $container->get(RemoteHelper::class) - ); - $object->setBuilder(CollectionBuilder::create($container, $object)); - - return $object; - } - - /** - * Sync the database and files between two sites. - * - * @param string $sourceUser - * SSH user to connect to the source server. - * @param string $sourceHost - * IP address of the source server. - * @param string $sourceKeyFile - * Private key file to use to connect to the source server. - * @param string $destinationUser - * SSH user to connect to the destination server. - * @param string $destinationHost - * IP address of the destination server. - * @param string $destinationKeyFile - * Private key file to use to connect to the destination server. - * @param string $sourceApp - * The name of the source app we're syncing. Used to determine the - * directory to sync. - * @param string $destinationApp - * The name of the destination app we're syncing. Used to determine the - * directory to sync to. - * - * @return \Robo\Contract\TaskInterface - * The sync task. - */ - public function syncTask( - $sourceUser, - $sourceHost, - $sourceKeyFile, - $destinationUser, - $destinationHost, - $destinationKeyFile, - $sourceApp = 'default', - $destinationApp = 'default', - $opts = ['files' => false, 'data' => false, 'rsync' => true] - ) { - if (!$opts['files'] && !$opts['data']) { - $opts['files'] = true; - $opts['data'] = true; - } - - $opts['rsync'] = !isset($opts['rsync']) || $opts['rsync']; - - $sourceRemote = $this->remoteHelper->getRemoteSettings( - $sourceHost, - $sourceUser, - $sourceKeyFile, - $sourceApp - ); - $sourceAuth = new KeyFile($sourceUser, $sourceKeyFile); - - $destinationRemote = $this->remoteHelper->getRemoteSettings( - $destinationHost, - $destinationUser, - $destinationKeyFile, - $destinationApp - ); - $destinationAuth = new KeyFile($destinationUser, $destinationKeyFile); - - $collection = $this->collectionBuilder(); - - if ($opts['files'] && $opts['rsync']) { - // Files are rsync'ed, no need to sync them through backups later. - $opts['files'] = false; - $collection->addTask( - $this->rsyncAllFilesTask( - $sourceAuth, - $sourceHost, - $sourceKeyFile, - $sourceRemote, - $destinationAuth, - $destinationHost, - $destinationKeyFile, - $destinationRemote - ) - ); - } - - if ($opts['data'] || $opts['files']) { - // Create a backup on the source host. - $collection->addTask( - $this->backupTaskFactory->backupTask( - $sourceHost, - $sourceAuth, - $sourceRemote, - $opts - ) - ); - // Download the backup from the source host to the local machine. - $collection->addTask( - $this->backupTaskFactory->downloadBackupTask( - $sourceHost, - $sourceAuth, - $sourceRemote, - $opts - ) - ); - // Remove the backup from the source host. - $collection->addTask( - $this->backupTaskFactory->removeBackupTask( - $sourceHost, - $sourceAuth, - $sourceRemote, - $opts - ) - ); - // Upload the backup to the destination host. - $collection->addTask( - $this->backupTaskFactory->uploadBackupTask( - $destinationHost, - $destinationAuth, - $destinationRemote, - $opts - ) - ); - // Restore the backup on the destination host. - $collection->addTask( - $this->backupTaskFactory->restoreBackupTask( - $destinationHost, - $destinationAuth, - $destinationRemote, - $opts - ) - ); - // Remove the backup from the destination host. - $collection->completion( - $this->backupTaskFactory->removeBackupTask( - $destinationHost, - $destinationAuth, - $destinationRemote, - $opts - ) - ); - - // Finally remove the local backups. - $dbBackupFile = $this->backupTaskFactory->backupFileName('.sql.gz', $sourceRemote['time']); - $removeLocalBackup = CommandBuilder::create('rm') - ->addFlag('f') - ->addArgument($dbBackupFile); - if ($opts['files']) { - $removeLocalBackup->addArgument($this->backupTaskFactory->backupFileName('.tar.gz', $sourceRemote['time'])); - } - - $collection->completion( - $this->taskExecStack() - ->exec((string) $removeLocalBackup) - ); - } - - if ($clearCache = $this->cacheTaskFactory->clearCacheTask($destinationHost, $destinationAuth, $destinationRemote)) { - $collection->completion($clearCache); - } - - return $collection; - } - - protected function rsyncAllFilesTask( - AbstractAuth $sourceAuth, - $sourceHost, - $sourceKeyFile, - $sourceRemote, - AbstractAuth $destinationAuth, - $destinationHost, - $destinationKeyFile, - $destinationRemote - ) { - $tmpKeyFile = '~/.ssh/' . uniqid('robo_', true) . '.id_rsa'; - $destinationUser = $destinationAuth->getUser(); - $sourceUser = $sourceAuth->getUser(); - $collection = $this->collectionBuilder(); - // Generate a temporary key. - $collection->addTask( - $this->generateKeyPair($tmpKeyFile) - ); - - $collection->completion( - $this->removeKeyPair($tmpKeyFile) - ); - - // Install it on the destination host. - $collection->addTask( - $this->installPublicKeyOnDestination( - $tmpKeyFile, - $destinationUser, - $destinationHost, - $destinationKeyFile - ) - ); - - // Remove it from the destination host when we're done. - $collection->completion( - $this->removePublicKeyFromDestination( - $tmpKeyFile, - $destinationHost, - $destinationAuth - ) - ); - - // Install the private key on the source host. - $collection->addTask( - $this->installPrivateKeyOnSource( - $tmpKeyFile, - $sourceHost, - $sourceUser, - $sourceKeyFile - ) - ); - - // Remove the private key from the source host. - $collection->completion( - $this->removePrivateKeyFromSource( - $tmpKeyFile, - $sourceHost, - $sourceAuth - ) - ); - - $backupConfig = $this->getBackupConfig(); - $dirs = ($backupConfig['file_backup_subdirs'] ? $backupConfig['file_backup_subdirs'] : ['']); - - foreach ($dirs as $dir) { - $dir .= ($dir !== '' ? '/' : ''); - $collection->addTask( - $this->rsyncDirectory( - $dir, - $tmpKeyFile, - $sourceHost, - $sourceAuth, - $sourceRemote, - $destinationHost, - $destinationAuth, - $destinationRemote - ) - ); - } - - return $collection; - } - - protected function generateKeyPair($privateKey) - { - return $this->taskExec( - (string) CommandBuilder::create('ssh-keygen') - ->addFlag('q') - ->addFlag('t', 'rsa') - ->addFlag('b', 4096) - ->addRawFlag('N', '""') - ->addRawFlag('f', $privateKey) - ->addFlag('C', 'robo:' . md5($privateKey)) - ); - } - - protected function removeKeyPair($privateKey) - { - return $this->taskExecStack() - ->exec( - (string) CommandBuilder::create('rm') - ->addFlag('f') - ->addRawArgument($privateKey) - ->addRawArgument($privateKey . '.pub') - ); - } - - protected function installPublicKeyOnDestination($privateKey, $destinationUser, $destinationHost, $destinationKeyFile) - { - return $this->taskExec( - (string) CommandBuilder::create('cat') - ->addRawArgument($privateKey . '.pub') - ->pipeOutputTo( - CommandBuilder::create('ssh') - ->addArgument($destinationUser . '@' . $destinationHost) - ->addFlag('o', 'StrictHostKeyChecking=no') - ->addRawFlag('i', $destinationKeyFile) - ) - ->addArgument( - CommandBuilder::create('mkdir') - ->addFlag('p') - ->addRawArgument('~/.ssh') - ->onSuccess( - CommandBuilder::create('cat') - ->chain('~/.ssh/authorized_keys', '>>') - ) - ) - ); - } - - protected function removePublicKeyFromDestination($privateKey, $destinationHost, AbstractAuth $destinationAuth) - { - return $this->taskSsh($destinationHost, $destinationAuth) - ->exec( - (string) CommandBuilder::create('sed') - ->addFlag('i', '/robo:' . md5($privateKey) . '/d') - ->addRawArgument('~/.ssh/authorized_keys') - ); - } - - protected function installPrivateKeyOnSource($privateKey, $sourceHost, $sourceUser, $sourceKeyFile) - { - return $this->taskRsync() - ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `vendor/bin/robo digipolis:realpath ' . $sourceKeyFile . '`"') - ->fromPath($privateKey) - ->toHost($sourceHost) - ->toUser($sourceUser) - ->toPath('~/.ssh') - ->archive() - ->compress() - ->checksum() - ->wholeFile(); - } - - protected function removePrivateKeyFromSource($privateKey, $sourceHost, AbstractAuth $sourceAuth) - { - return $this->taskSsh($sourceHost, $sourceAuth) - ->exec( - (string) CommandBuilder::create('rm') - ->addFlag('f') - ->addRawArgument($privateKey) - ); - } - - protected function rsyncDirectory($dir, $privateKey, $sourceHost, AbstractAuth $sourceAuth, $sourceSettings, $destinationHost, AbstractAuth $destinationAuth, $destinationSettings) - { - $rsync = $this->taskRsync() - ->rawArg('--rsh "ssh -o StrictHostKeyChecking=no -i `cd -P ' . $sourceSettings['currentdir'] . '/.. && vendor/bin/robo digipolis:realpath ' . $privateKey . '`"') - ->fromPath($sourceSettings['filesdir'] . '/' . $dir) - ->toHost($destinationHost) - ->toUser($destinationAuth->getUser()) - ->toPath($destinationSettings['filesdir'] . '/' . $dir) - ->archive() - ->delete() - ->rawArg('--copy-links --keep-dirlinks') - ->compress() - ->checksum() - ->wholeFile(); - $backupConfig = $this->getBackupConfig(); - foreach ($backupConfig['exclude_from_backup'] as $exclude) { - $rsync->exclude($exclude); - } - - return $this->taskSsh($sourceHost, $sourceAuth) - ->timeout($this->remoteHelper->getTimeoutSetting('synctask_rsync')) - ->exec($rsync); - } -} diff --git a/src/Util/TimeHelper.php b/src/Util/TimeHelper.php new file mode 100644 index 0000000..b2a1603 --- /dev/null +++ b/src/Util/TimeHelper.php @@ -0,0 +1,47 @@ +time = time(); + } + + /** + * Get the singleton instance. + * + * @return static + */ + public static function getInstance(): static + { + if (!static::$instance) { + static::$instance = new static(); + } + return static::$instance; + } + + /** + * Get the timestamp. + * + * @return int + */ + public function getTime() + { + return $this->time; + } +} diff --git a/src/default.properties.yml b/src/default.properties.yml new file mode 100644 index 0000000..989066d --- /dev/null +++ b/src/default.properties.yml @@ -0,0 +1,21 @@ +remote: + appdir: '/home/[user]/apps/[app]' + releasesdir: '${remote.appdir}/releases' + rootdir: '${remote.releasesdir}/[time]' + webdir: '${remote.rootdir}' + currentdir: '${remote.appdir}/current' + configdir: '${remote.appdir}/config' + filesdir: '${remote.appdir}/files' + backupsdir: '${remote.appdir}/backups' + createbackup: false + symlinks: + - '${remote.webdir}:${remote.currentdir}' + opcache: + env: 'fcgi' + host: '/usr/local/multi-php/[user]/run/[user].sock' + cleandir_limit: 2 + postsymlink_filechecks: + - '${remote.rootdir}/vendor/autoload.php' + environment_overrides: + ^staging: + cleandir_limit: 1