diff --git a/README.md b/README.md index 37194f3..2b3aa5e 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] for all default values, and +[the properties.yml documentation] for all available +configuration options. + +By default, we assume a [capistrano-like directory 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]. The +default event listeners provided by this package are in the +[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]. 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] +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]. ### 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,496 @@ 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*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] +*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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*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] +*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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - sourceRemoteConfig: The [RemoteConfig] object + with data relevant to the source host and app. + - destinationRemoteConfig: The [RemoteConfig] + 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +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] +will be executed from your deployment server, or your local machine, and thus +will need an ssh connection to the host. + +*Default Handler*: [SwitchPreviousHandler] +*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] +*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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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] +*Event arguments*: + - remoteConfig: The [RemoteConfig] 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. + + +default.properties.yml: src/default.properties.yml +the properties.yml documentation: #propertiesyml +capistrano-like directory structure: http://capistranorb.com/documentation/getting-started/structure/ +EventHandlerWithPriority: src/EventHandler/EventHandlerWithPriority +DigipolisHelpersDefaultHooksCommands: src/Robo/Plugin/Commands/DigipolisHelpersDefaultHooksCommands +src/EventHandler/DefaultHandler: src/EventHandler/DefaultHandler +create an event listener by using the on-event hook: https://github.com/consolidation/annotated-command#on-event-hook +list of available events: #list-of-available-events +RemoteConfig: src/Util/RemoteConfig.php +BackupRemoteHandler: src/EventHandler/DefaultHandler/BackupRemoteHandler.php +BuildTaskHandler: src/EventHandler/DefaultHandler/BuildTaskHandler.php +CleanDirsHandler: src/EventHandler/DefaultHandler/CleanDirsHandler.php +ClearCacheHandler: src/EventHandler/DefaultHandler/ClearCacheHandler.php +ClearRemoteOpcacheHandler: src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php +CompressOldReleaseHandler: src/EventHandler/DefaultHandler/CompressOldReleaseHandler.php +CurrentProjectRootHandler: src/EventHandler/DefaultHandler/CurrentProjectRootHandler.php +DownloadBackupHandler: src/EventHandler/DefaultHandler/DownloadBackupHandler.php +InstallHandler: src/EventHandler/DefaultHandler/InstallHandler.php +IsSiteInstalledHandler: src/EventHandler/DefaultHandler/IsSiteInstalledHandler.php +LocalSettingsHandler: src/EventHandler/DefaultHandler/LocalSettingsHandler.php +MirrorDirHandler: src/EventHandler/DefaultHandler/MirrorDirHandler.php +PostSymlinkHandler: src/EventHandler/DefaultHandler/PostSymlinkHandler.php +PreLocalSyncFilesHandler: src/EventHandler/DefaultHandler/PreLocalSyncFilesHandler.php +PreRestoreBackupRemoteHandler: src/EventHandler/DefaultHandler/PreRestoreBackupRemoteHandler.php +PreSymlinkHandler: src/EventHandler/DefaultHandler/PreSymlinkHandler.php +PushPackageHandler: src/EventHandler/DefaultHandler/PushPackageHandler.php +RealpathHandler: src/EventHandler/DefaultHandler/RealpathHandler.php +RemoteSettingsHandler: src/EventHandler/DefaultHandler/RemoteSettingsHandler.php +RemoteSwitchPreviousHandler: src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php +RemoteSymlinkPreviousHandler: src/EventHandler/DefaultHandler/RemoteSymlinkPreviousHandler.php +RemoveBackupRemoteHandler: src/EventHandler/DefaultHandler/RemoveBackupRemoteHandler.php +RemoveFailedReleaseHandler: src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php +RemoveLocalBackupHandler: src/EventHandler/DefaultHandler/RemoveLocalBackupHandler.php +RestoreBackupDbLocalHandler: src/EventHandler/DefaultHandler/RestoreBackupDbLocalHandler.php +RestoreBackupFilesLocalHandler: src/EventHandler/DefaultHandler/RestoreBackupFilesLocalHandler.php +RestoreBackupRemoteHandler: src/EventHandler/DefaultHandler/RestoreBackupRemoteHandler.php +RsyncFilesBetweenHostsHandler: src/EventHandler/DefaultHandler/RsyncFilesBetweenHostsHandler.php +RsyncFilesToLocalHandler: src/EventHandler/DefaultHandler/RsyncFilesToLocalHandler.php +digipolis:remote-switch-previous: #digipolis-remote-switch-previous +SwitchPreviousHandler: src/EventHandler/DefaultHandler/SwitchPreviousHandler.php +TimeoutSettingHandler: src/EventHandler/DefaultHandler/TimeoutSettingHandler.php +UpdateHandler: src/EventHandler/DefaultHandler/UpdateHandler.php +UploadBackupHandler: src/EventHandler/DefaultHandler/UploadBackupHandler.php 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..6d9f3bc --- /dev/null +++ b/src/EventHandler/DefaultHandler/CleanDirsHandler.php @@ -0,0 +1,35 @@ +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..3d555cc --- /dev/null +++ b/src/EventHandler/DefaultHandler/ClearRemoteOpcacheHandler.php @@ -0,0 +1,36 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $timeouts = $event->getArgument('timeout'); + $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..bb09bc7 --- /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..4caa705 --- /dev/null +++ b/src/EventHandler/DefaultHandler/MirrorDirHandler.php @@ -0,0 +1,45 @@ +getArgument('dir'); + $destination = $event->getArgument('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; + } +} 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..f6938f9 --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoteSwitchPreviousHandler.php @@ -0,0 +1,32 @@ +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..38caeef --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoteSymlinkHandler.php @@ -0,0 +1,44 @@ +getArgument('remoteConfig'); + $remoteSettings = $remoteConfig->getRemoteSettings(); + $auth = new KeyFile($remoteConfig->getUser(), $remoteConfig->getPrivateKeyFile()); + + $collection = $this->collectionBuilder(); + foreach ($remoteSettings['symlinks'] as $link) { + $preIndividualSymlinkTask = $this->preIndividualSymlinkTask($remoteConfig, $link); + 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..c5855ed --- /dev/null +++ b/src/EventHandler/DefaultHandler/RemoveFailedReleaseHandler.php @@ -0,0 +1,31 @@ +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', + ] + ) { + // 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) + { + $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, + ] + ); + } + + /** + * 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/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..5419276 --- /dev/null +++ b/src/Robo/Plugin/Commands/DigipolisHelpersSyncCommand.php @@ -0,0 +1,179 @@ + 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/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/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