diff --git a/.env b/.env index cdc6058a..2148dfc4 100644 --- a/.env +++ b/.env @@ -1,5 +1,3 @@ # This file is a "template" of which env vars need to be defined for your application # Copy this file to .env file for development, create environment variables when deploying to production # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration -SHELL_VERBOSITY=0 -APP_ENV=debug \ No newline at end of file diff --git a/composer.json b/composer.json index d9c5f29a..b6e60bf0 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "ext-openssl": "*", "jakeasmith/http_build_url": "^1.0", "padraic/phar-updater": "^1.0", - "lesstif/php-jira-rest-client": "^1.35" + "lesstif/php-jira-rest-client": "^1.35", + "graze/parallel-process": "^0.8.1" }, "require-dev": { "symfony/phpunit-bridge": "^2.8|^3|^4.1", diff --git a/composer.lock b/composer.lock index 63f4b1ea..fb2c8141 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f7bccafd33b2508f929c7c85efb734c2", + "content-hash": "11989c13bcb0588ac113f3373ef4ba47", "packages": [ { "name": "composer/ca-bundle", @@ -124,6 +124,179 @@ ], "time": "2016-08-30T16:08:34+00:00" }, + { + "name": "graze/data-structure", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/graze/data-structure.git", + "reference": "24e0544b7828f65b1b93ce69ad702c9efb4a64d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/graze/data-structure/zipball/24e0544b7828f65b1b93ce69ad702c9efb4a64d0", + "reference": "24e0544b7828f65b1b93ce69ad702c9efb4a64d0", + "shasum": "" + }, + "require": { + "graze/sort": "~2.0", + "php": ">=5.5|^7.0" + }, + "require-dev": { + "graze/standards": "^2.0", + "phpunit/phpunit": "^4.2 | ^5.2", + "squizlabs/php_codesniffer": "^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Graze\\DataStructure\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graze tech team", + "homepage": "https://github.com/graze/data-structure/graphs/contributors" + } + ], + "description": "Data collections and containers", + "homepage": "https://github.com/graze/data-structure", + "keywords": [ + "array", + "collection", + "container", + "data", + "filter", + "map", + "parameters", + "reduce", + "structure" + ], + "time": "2017-11-29T09:06:31+00:00" + }, + { + "name": "graze/parallel-process", + "version": "0.8.1", + "source": { + "type": "git", + "url": "https://github.com/graze/parallel-process.git", + "reference": "84ccec5d8a62a8ed928dc5e1c0da8f646bcef7cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/graze/parallel-process/zipball/84ccec5d8a62a8ed928dc5e1c0da8f646bcef7cc", + "reference": "84ccec5d8a62a8ed928dc5e1c0da8f646bcef7cc", + "shasum": "" + }, + "require": { + "graze/data-structure": "^2.0", + "php": "^5.5 | ^7.0", + "psr/log": "^1.0", + "symfony/event-dispatcher": "^2.8 | ^3.2 | ^4.0", + "symfony/process": "^2.8 | ^3.2 | ^4.0" + }, + "require-dev": { + "graze/console-diff-renderer": "^0.6.1", + "graze/standards": "^2", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^5.7.21|^6|^7", + "squizlabs/php_codesniffer": "^3", + "symfony/console": "^3.1 | ^4" + }, + "suggest": { + "graze/console-diff-renderer": "required to use Table and Lines", + "symfony/console": "To use the Table to print current runs" + }, + "type": "library", + "autoload": { + "psr-4": { + "Graze\\ParallelProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Harry Bragg", + "email": "harry.bragg@graze.com", + "role": "Developer" + }, + { + "name": "Graze Developers", + "email": "developers@graze.com", + "homepage": "http://www.graze.com", + "role": "Development Team" + } + ], + "description": "run a pool of processes simultaneously", + "homepage": "https://github.com/graze/parallel-process", + "keywords": [ + "graze", + "parallel-process" + ], + "time": "2018-09-25T09:06:26+00:00" + }, + { + "name": "graze/sort", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/graze/sort.git", + "reference": "50f0896363f177f68be248d7bad9eb0c2f7f666c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/graze/sort/zipball/50f0896363f177f68be248d7bad9eb0c2f7f666c", + "reference": "50f0896363f177f68be248d7bad9eb0c2f7f666c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "adlawson/timezone": "~1.0", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/fn.php" + ], + "psr-4": { + "Graze\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graze tech team", + "homepage": "https://github.com/graze/sort/graphs/contributors" + } + ], + "description": "A collection of array sorting transforms and functions", + "homepage": "https://github.com/graze/sort", + "keywords": [ + "array", + "collection", + "list", + "order", + "ordered", + "schwartzian", + "sort", + "sorting", + "transform" + ], + "time": "2014-09-23T17:01:23+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "6.3.3", diff --git a/docs/docs/available-tasks.md b/docs/docs/available-tasks.md index 3acab3ca..59506fe2 100644 --- a/docs/docs/available-tasks.md +++ b/docs/docs/available-tasks.md @@ -521,7 +521,8 @@ phab --config= notify This command will send the notification to Mattermosts channel . For a detailed description have a look into the dedicated documentation. **Examples** -* `phab config:mbb notify "hello world" "off-topic": sends `hello world` to `#off-topic` + +* `phab config:mbb notify "hello world" "off-topic"`: sends `hello world` to `#off-topic` ## app:scaffold diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index dbd3ba5a..54bbeadb 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -197,6 +197,10 @@ This will print all host configuration for the host `staging`. * `name` contains the name of the docker-container. This is needed to get the IP-address of the particular docker-container when using ssh-tunnels (see above). * for docker-compose-base setups you can provide the `service` instead the name, phabalicious will get the docker name automatically from the service. +### Configuration of the mattermost-method + +* `notifyOn`: a list of all tasks where to send a message to a Mattermost channel. Have a look at the global Mattermost-configuration-example below. + ### dockerHosts `dockerHosts` is similar structured as the `hosts`-entry. It's a keyed lists of hosts containing all necessary information to create a ssh-connection to the host, controlling the docker-instances, and a list of tasks, the user might call via the `docker`-command. See the `docker`-entry for a more birds-eye-view of the concepts. @@ -321,6 +325,37 @@ jira: projectKey: ``` +### mattermost + +Phabalicious can send notifications to a running Mattermost instance. You need to create an incoming web hook in your instance and pass this to your configuration. Here's an example + +``` +mattermost: + username: phabalicious + webhook: https://chat.your.server.tld/hooks/... + Channel: "my-channel" + +hosts: + test: + needs: + - mattermost + notifyOn: + - deploy + - reset +``` + +* `mattermost` contains all global mattermost config. + * `username` the username to post messages as + * `webhook` the address of the web-hook + * `channel` the channel to post the message to +* `notifyOn` is a list of tasks which should send a notification + +You can test the Mattermost config via + +``` +phab notify "hello world" --config +``` + ### other * `deploymentModule` name of the deployment-module the drush-method enables when doing a deploy diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index bf93c55f..f56040ef 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -4,20 +4,17 @@ use Phabalicious\Configuration\ConfigurationService; use Phabalicious\Configuration\HostConfig; -use Phabalicious\Exception\BlueprintTemplateNotFoundException; -use Phabalicious\Exception\FabfileNotFoundException; -use Phabalicious\Exception\FabfileNotReadableException; -use Phabalicious\Exception\MismatchedVersionException; use Phabalicious\Exception\ValidationFailedException; use Phabalicious\Exception\MissingHostConfigException; use Phabalicious\ShellProvider\ShellProviderInterface; +use Phabalicious\Utilities\ParallelExecutor; use Psr\Log\NullLogger; -use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Process\Process; abstract class BaseCommand extends BaseOptionsCommand @@ -47,6 +44,20 @@ protected function configure() InputOption::VALUE_OPTIONAL, 'Which blueprint to use', null + ) + ->addOption( + 'variants', + null, + InputOption::VALUE_OPTIONAL, + 'Run the command on a given set of blueprints simultanously', + null + ) + ->addOption( + 'force', + null, + InputOption::VALUE_OPTIONAL, + 'Don\'t ask for confirmation', + false ); parent::configure(); @@ -78,6 +89,8 @@ public function completeOptionValues($optionName, CompletionContext $context) */ protected function execute(InputInterface $input, OutputInterface $output) { + $io = new SymfonyStyle($input, $output); + $this->checkAllRequiredOptionsAreNotEmpty($input); $config_name = '' . $input->getOption('config'); @@ -102,24 +115,25 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($this->hostConfig->shell()) { $this->hostConfig->shell()->setOutput($output); } + + if ($input->getOption('variants')) { + return $this->handleVariants($input->getOption('variants'), $input, $output); + } } catch (MissingHostConfigException $e) { - $output->writeln('Could not find host-config named `' . $config_name . '`'); + $io->error(sprintf('Could not find host-config named `%s`', $config_name)); return 1; } catch (ValidationFailedException $e) { - $output->writeln('Could not validate config `' . $config_name . '`'); - foreach ($e->getValidationErrors() as $error_msg) { - $output->writeln('' . $error_msg . ''); - } + $io->error(sprintf( + "Could not validate config `%s`\n\n%s", + $config_name, + implode("\n", $e->getValidationErrors()) + )); return 1; } return 0; } - - - - /** * Get host config. * @@ -188,4 +202,85 @@ protected function startInteractiveShell(ShellProviderInterface $shell, array $c return $process; } + + /** + * Handle variants. + * + * @param $variants + * @param InputInterface $input + * @param OutputInterface $output + * @return bool|int + */ + private function handleVariants($variants, InputInterface $input, OutputInterface $output) + { + global $argv; + $executable = $argv[0]; + if (basename($executable) !== 'phab') { + $executable = 'bin/phab'; + } + if (getenv('PHABALICIOUS_EXECUTABLE')) { + $executable = getenv('PHABALICIOUS_EXECUTABLE'); + } + + $available_variants = $this->configuration->getBlueprints()->getVariants($this->hostConfig['configName']); + if (!$available_variants) { + throw new \InvalidArgumentException(sprintf( + 'Could not find variants for `%s` in `blueprints`', + $this->hostConfig['configName'] + )); + } + + if ($variants == 'all') { + $variants = $available_variants; + } else { + $variants = explode(',', $variants); + $not_found = array_filter($variants, function ($v) use ($available_variants) { + return !in_array($v, $available_variants); + }); + + if (!empty($not_found)) { + throw new \InvalidArgumentException(sprintf( + 'Could not find variants `%s` in `blueprints`', + implode('`, `', $not_found) + )); + } + } + if (!empty($variants)) { + $cmd_lines = []; + $rows = []; + foreach ($variants as $v) { + $cmd = []; + $cmd[] = $executable; + + foreach ($input->getArguments() as $a) { + $cmd[] = $a; + } + foreach ($input->getOptions() as $name => $value) { + if ($value && !in_array($name, ['verbose', 'variants', 'blueprint', 'fabfile'])) { + $cmd[] = '--' . $name; + $cmd[]= $value; + } + } + $cmd[] = '--no-interaction'; + $cmd[] = '--fabfile'; + $cmd[] = $this->configuration->getFabfileLocation(); + $cmd[] = '--blueprint'; + $cmd[] = $v; + + $cmd_lines[] = $cmd; + $rows[] = [$v, implode(' ', $cmd)]; + } + + $io = new SymfonyStyle($input, $output); + $io->table(['variant', 'command'], $rows); + + if ($input->getOption('force') || $io->confirm('Do you want to run these commands? ', false)) { + $io->comment('Running ...'); + $executor = new ParallelExecutor($cmd_lines, $output); + return $executor->execute($input, $output); + } + + return 1; + } + } } diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index f130b593..2aa91868 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -25,13 +25,6 @@ protected function configure() 'Skip the reset-task if set to true', false ); - $this->addOption( - 'yes', - 'y', - InputOption::VALUE_OPTIONAL, - 'Skip confirmation step, install without question', - false - ); } /** @@ -61,7 +54,9 @@ protected function execute(InputInterface $input, OutputInterface $output) throw new \InvalidArgumentException('This configuration disallows installs!'); } - if (!$input->getOption('yes')) { + $context = new TaskContext($this, $input, $output); + + if (!$input->getOption('force')) { if (!$context->io()->confirm(sprintf( 'Install new database for configuration `%s`?', $this->getHostConfig()['configName'] diff --git a/src/Command/OutputCommand.php b/src/Command/OutputCommand.php index f98149a5..7ad3b136 100644 --- a/src/Command/OutputCommand.php +++ b/src/Command/OutputCommand.php @@ -45,19 +45,24 @@ protected function execute(InputInterface $input, OutputInterface $output) $template = $this->getConfiguration()->getBlueprints()->getTemplate($config); $data = $template->expand($blueprint); - $data = ['hosts' => [ + $data = [ $data['configName'] => $data - ]]; + ]; $dumper = new Dumper(2); $io = new SymfonyStyle($input, $output); - $io->title('Output of applied blueprint `' . $config . '`'); - $io->block($dumper->dump($data, 10, 2)); + if ($output->isDecorated()) { + $io->title('Output of applied blueprint `' . $config . '`'); + } + $io->block( + $dumper->dump($data, 10, 2), + null, + null, + '' + ); return 0; } - - -} \ No newline at end of file +} diff --git a/src/Command/PutFileCommand.php b/src/Command/PutFileCommand.php index a845f14c..d141429c 100644 --- a/src/Command/PutFileCommand.php +++ b/src/Command/PutFileCommand.php @@ -55,10 +55,17 @@ protected function execute(InputInterface $input, OutputInterface $output) $context = new TaskContext($this, $input, $output); $context->set('sourceFile', $file); - $output->writeln('Put file `' . $file . '` into `' . $this->getHostConfig()['configName']. '`'); + $context->io()->comment('Putting file `' . $file . '` to `' . $this->getHostConfig()['configName']. '`'); $this->getMethods()->runTask('putFile', $this->getHostConfig(), $context); - return $context->getResult('exitCode', 0); + $return_code = $context->getResult('exitCode', 0); + if (!$return_code) { + $context->io()->success(sprintf( + '`%s` copied to `%s`', + $file, + $context->getResult('targetFile', 'unknown') + )); + } } } diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php index 6467c08a..9c5152d0 100644 --- a/src/Command/SelfUpdateCommand.php +++ b/src/Command/SelfUpdateCommand.php @@ -67,7 +67,7 @@ protected static function getUpdater(Application $application, $allow_unstable) private function runSelfUpdate($allow_unstable) { - $update_data = $this->hasUpdate(); + $update_data = $this->hasUpdate($allow_unstable); if (!$update_data) { return false; } @@ -77,11 +77,14 @@ private function runSelfUpdate($allow_unstable) return $result ? $updater->getNewVersion() : false; } - public function hasUpdate() + public function hasUpdate($allow_unstable = false) { try { $version = $this->getApplication()->getVersion(); - $allow_unstable = (stripos($version, 'alpha') !== false) || (stripos($version, 'beta') !== false); + $allow_unstable = + $allow_unstable || + (stripos($version, 'alpha') !== false) || + (stripos($version, 'beta') !== false); $updater = self::getUpdater($this->getApplication(), $allow_unstable); @@ -117,9 +120,12 @@ public static function registerListener(EventDispatcher $dispatcher) $command = $event->getCommand()->getApplication()->find('self-update'); if ($command + && $output->isDecorated() + && !$output->isQuiet() && !$event->getCommand()->isHidden() - && !$output->isQuiet() && !$command->getConfiguration()->isOffline() + && !$command->getConfiguration()->isOffline() && !$input->hasParameterOption(['--offline']) + && !$input->hasParameterOption(['--no-interaction']) ) { if ($version = $command->hasUpdate()) { $style = new SymfonyStyle($input, $output); diff --git a/src/Command/ShellCommandCommand.php b/src/Command/ShellCommandCommand.php index 6c5651fe..31411a4a 100644 --- a/src/Command/ShellCommandCommand.php +++ b/src/Command/ShellCommandCommand.php @@ -5,6 +5,8 @@ use Phabalicious\Configuration\ConfigurationService; use Phabalicious\Configuration\HostConfig; use Phabalicious\Method\TaskContext; +use Phabalicious\Method\TaskContextInterface; +use Phabalicious\ShellProvider\ShellProviderInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -48,9 +50,13 @@ protected function execute(InputInterface $input, OutputInterface $output) // Allow methods to override the used shellProvider: $this->getMethods()->runTask('shell', $host_config, $context); + + /** @var ShellProviderInterface $shell */ $shell = $context->getResult('shell', $host_config->shell()); + $ssh_command = $context->getResult('ssh_command', $shell->getShellCommand()); - $output->writeln(implode(' ', $shell->getShellCommand())); - } + $context->io()->text('$ ' . implode(' ', $ssh_command)); -} \ No newline at end of file + return 0; + } +} diff --git a/src/Configuration/BlueprintConfiguration.php b/src/Configuration/BlueprintConfiguration.php index e24e3d13..fe91abfd 100644 --- a/src/Configuration/BlueprintConfiguration.php +++ b/src/Configuration/BlueprintConfiguration.php @@ -30,7 +30,6 @@ public function __construct(ConfigurationService $service) } } foreach ($this->configuration->getAllHostConfigs() as $key => $data) { - if (!empty($data['blueprint'])) { $this->templates['host:' . $key] = new BlueprintTemplate($this->configuration, $data['blueprint']); } @@ -91,4 +90,23 @@ public function expandVariants($blueprints) } } } -} \ No newline at end of file + + + /** + * Get all variants for a given config. + * + * @param $config_name + * @return bool|array + */ + public function getVariants($config_name) + { + $data = $this->configuration->getSetting('blueprints', []); + foreach ($data as $b) { + if ($b['configName'] == $config_name) { + return $b['variants']; + } + } + + return false; + } +} diff --git a/src/Configuration/ConfigurationService.php b/src/Configuration/ConfigurationService.php index 8781b836..97d73844 100644 --- a/src/Configuration/ConfigurationService.php +++ b/src/Configuration/ConfigurationService.php @@ -35,6 +35,7 @@ class ConfigurationService private $methods; private $fabfilePath; + private $fabfileLocation; private $dockerHosts; private $hosts; @@ -104,6 +105,7 @@ public function readConfiguration(string $path, string $override = ''): bool } $this->setFabfilePath(dirname($fabfile)); + $this->fabfileLocation = $fabfile; $data = $this->readFile($fabfile); if (!$data) { @@ -232,6 +234,11 @@ public function getFabfilePath() return $this->fabfilePath; } + public function getFabfileLocation() + { + return $this->fabfileLocation; + } + public function mergeData(array $data, array $override_data): array { return Utilities::mergeData($data, $override_data); diff --git a/src/Method/DrushMethod.php b/src/Method/DrushMethod.php index d68aae87..8f9fde31 100644 --- a/src/Method/DrushMethod.php +++ b/src/Method/DrushMethod.php @@ -84,8 +84,14 @@ public function getDefaultConfig(ConfigurationService $configuration_service, ar $config['database']['prefix'] = false; } - $config['drupalVersion'] = in_array('drush7', $host_config['needs']) ? 7 : 8; - $config['drushVersion'] = in_array('drush9', $host_config['needs']) ? 9 : 8; + $config['drupalVersion'] = in_array('drush7', $host_config['needs']) + ? 7 + : $configuration_service->getSetting('drupalVersion', 8); + + $config['drushVersion'] = in_array('drush9', $host_config['needs']) + ? 9 + : $configuration_service->getSetting('drushVersion', 8); + $config['supportsZippedBackups'] = true; $config['siteFolder'] = 'sites/default'; $config['filesFolder'] = 'sites/default/files'; diff --git a/src/Method/GitMethod.php b/src/Method/GitMethod.php index 8249af16..087ee2dd 100644 --- a/src/Method/GitMethod.php +++ b/src/Method/GitMethod.php @@ -42,7 +42,7 @@ public function getDefaultConfig(ConfigurationService $configuration_service, ar { return [ 'branch' => 'develop', - 'gitRootFolder' => $host_config['rootFolder'], + 'gitRootFolder' => $host_config['rootFolder'] ?? null, 'ignoreSubmodules' => false, 'gitOptions' => $configuration_service->getSetting('gitOptions', []), ]; diff --git a/src/Method/SshMethod.php b/src/Method/SshMethod.php index 9e765e10..0eb87a99 100644 --- a/src/Method/SshMethod.php +++ b/src/Method/SshMethod.php @@ -7,7 +7,6 @@ use Phabalicious\ShellProvider\ShellProviderFactory; use Phabalicious\ShellProvider\SshShellProvider; use Phabalicious\Validation\ValidationErrorBagInterface; -use webignition\ReadableDuration\ReadableDuration; class SshMethod extends BaseMethod implements MethodInterface { @@ -172,4 +171,30 @@ public function preflightTask(string $task, HostConfig $config, TaskContextInter $this->creatingTunnel = false; } -} \ No newline at end of file + public function shell(HostConfig $config, TaskContextInterface $context) + { + if (!empty($config['sshTunnel'])) { + $tunnel = $config['sshTunnel']; + $ssh_command = [ + 'ssh', + '-A', + '-J', + sprintf( + '%s@%s:%s', + $tunnel['bridgeUser'], + $tunnel['bridgeHost'], + $tunnel['bridgePort'] + ), + '-p', + $tunnel['destPort'], + sprintf( + '%s@%s', + $config['user'], + $tunnel['destHost'] + ) + ]; + + $context->setResult('ssh_command', $ssh_command); + } + } +} diff --git a/src/ShellCompletion/FishShellCompletionDescriptor.php b/src/ShellCompletion/FishShellCompletionDescriptor.php index e245c8bb..14105240 100644 --- a/src/ShellCompletion/FishShellCompletionDescriptor.php +++ b/src/ShellCompletion/FishShellCompletionDescriptor.php @@ -86,9 +86,8 @@ protected function describeInputOption(InputOption $option, array $options = arr ); $this->output->writeln( - " -d '" . - $option->getDescription() . - "'" + " -d " . + escapeshellarg($option->getDescription()) ); if ($command instanceof CompletionAwareInterface) { global $argv; diff --git a/src/ShellProvider/LocalShellProvider.php b/src/ShellProvider/LocalShellProvider.php index 7b0de5c2..db8d2ba0 100644 --- a/src/ShellProvider/LocalShellProvider.php +++ b/src/ShellProvider/LocalShellProvider.php @@ -218,6 +218,8 @@ public function putFile(string $source, string $dest, TaskContextInterface $cont { $this->cd($context->getConfigurationService()->getFabfilePath()); $result = $this->run(sprintf('cp -r "%s" "%s"', $source, $dest)); + $context->setResult('targetFile', $dest); + return $result->succeeded(); } diff --git a/src/ShellProvider/SshShellProvider.php b/src/ShellProvider/SshShellProvider.php index 2d5f496f..bf62f62d 100644 --- a/src/ShellProvider/SshShellProvider.php +++ b/src/ShellProvider/SshShellProvider.php @@ -128,6 +128,8 @@ public function putFile(string $source, string $dest, TaskContextInterface $cont $command[] = $source; $command[] = $this->hostConfig['user'] . '@' . $this->hostConfig['host'] . ':' . $dest; + $context->setResult('targetFile', $dest); + return $this->runProcess($command, $context, false, true); } @@ -263,9 +265,7 @@ public function copyFileFrom( } else { $this->logger->warning('Could not copy file via SSH, try fallback'); } - } return parent::copyFileFrom($from_shell, $source_file_name, $target_file_name, $context, $verbose); } - } diff --git a/src/Utilities/Logger.php b/src/Utilities/Logger.php index d8bfb8d1..1ece9ca3 100644 --- a/src/Utilities/Logger.php +++ b/src/Utilities/Logger.php @@ -6,6 +6,7 @@ use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Logger\ConsoleLogger; +use Symfony\Component\Console\Output\Output; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -24,7 +25,7 @@ class Logger extends ConsoleLogger LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, - LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, + LogLevel::WARNING => OutputInterface::VERBOSITY_VERBOSE, LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE, LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, @@ -59,6 +60,10 @@ public function __construct(InputInterface $input, OutputInterface $output) LogLevel::INFO => self::INFO, LogLevel::DEBUG => self::DEBUG, ]; + if (!$output->isDecorated()) { + // For undecorated output warnings will be shown only when using verbose mode. + $this->verbosityLevelMap[LogLevel::WARNING] = OutputInterface::VERBOSITY_VERBOSE; + } parent::__construct( $output, $this->verbosityLevelMap, diff --git a/src/Utilities/ParallelExecutor.php b/src/Utilities/ParallelExecutor.php new file mode 100644 index 00000000..777624bc --- /dev/null +++ b/src/Utilities/ParallelExecutor.php @@ -0,0 +1,97 @@ +pool = new PriorityPool(); + $this->pool->setMaxSimultaneous($max_simultaneous_processes); + + foreach ($command_lines as $cmd) { + $this->add(new ParallelExecutorRun( + $cmd, + $output instanceof ConsoleOutput + ? $output->section() + : null + )); + } + } + + public function execute(InputInterface $input, OutputInterface $output) + { + + $progress_section = $output instanceof ConsoleOutput + ? $output->section() + : $output; + $progress = new ProgressBar($progress_section, $this->pool->count()); + + $this->pool->start(); + $output->writeln(''); + $progress->display(); + + $interval = (200000); + $current = 0; + $previous = 0; + while ($this->pool->poll()) { + usleep($interval); + $p = $this->pool->getProgress(); + $current = $p[0]; + $progress->advance($current - $previous); + $previous = $current; + } + $progress->finish(); + $style = new SymfonyStyle($input, $output); + + foreach ($this->pool->getAll() as $run) { + if ($run instanceof ParallelExecutorRun) { + $style->section(sprintf('Results of `%s`', $run->getCommandLine())); + $exceptions = $run->getExceptions(); + + if (count($exceptions) > 0) { + $exception = reset($exceptions); + $style->error(sprintf( + "Execution failed with error %d:\n%s", + $exception->getCode(), + $exception->getMessage() + )); + } else { + $style->writeln($run->getProcess()->getOutput()); + } + } + } + + return $this->pool->isSuccessful(); + } + + public function add(ParallelExecutorRun $run) + { + $this->pool->add($run); + } + + private function format(RunInterface $run, string $message) + { + if ($run instanceof ProcessRun) { + $cmd = $run->getProcess()->getCommandLine(); + return implode(' ', $cmd) . ': ' . $message; + } + return $message; + } +} diff --git a/src/Utilities/ParallelExecutorRun.php b/src/Utilities/ParallelExecutorRun.php new file mode 100644 index 00000000..a4ebdf62 --- /dev/null +++ b/src/Utilities/ParallelExecutorRun.php @@ -0,0 +1,65 @@ +output = $output; + $this->commandLine = implode(' ', $command_line); + + parent::__construct(new Process($command_line)); + if ($output) { + $this->addListeners(); + } + } + + public function addListeners() + { + $this->writeln("Waiting"); + + $this->addListener( + RunEvent::STARTED, + function (RunEvent $event) { + $this->writeln("→ Started"); + } + ); + $this->addListener( + RunEvent::SUCCESSFUL, + function (RunEvent $event) { + $this->writeln("✓ Succeeded"); + } + ); + $this->addListener( + RunEvent::FAILED, + function (RunEvent $event) { + $error = "x Failed"; + $this->writeln($error); + } + ); + } + + public function writeln($message) + { + $this->output->overwrite($this->commandLine . ': ' . $message); + } + + public function getCommandLine() + { + return $this->commandLine; + } +} diff --git a/symfony.lock b/symfony.lock index 4d61c8d5..7490a874 100644 --- a/symfony.lock +++ b/symfony.lock @@ -8,6 +8,15 @@ "doctrine/instantiator": { "version": "1.1.0" }, + "graze/data-structure": { + "version": "2.1.0" + }, + "graze/parallel-process": { + "version": "0.8.1" + }, + "graze/sort": { + "version": "2.0.1" + }, "guzzlehttp/guzzle": { "version": "6.3.3" }, diff --git a/tests/InstallCommandTest.php b/tests/InstallCommandTest.php index 10e3440d..1a9d628d 100644 --- a/tests/InstallCommandTest.php +++ b/tests/InstallCommandTest.php @@ -41,7 +41,7 @@ public function testSupportsInstallsOnProd() $commandTester->execute(array( 'command' => $command->getName(), '--config' => 'testProd', - '--yes' => true, + '--force' => true, )); // the output of the command in the console @@ -56,7 +56,7 @@ public function testSupportsInstallsOnStage() $commandTester->execute(array( 'command' => $command->getName(), '--config' => 'testStage', - '--yes' => true, + '--force' => true, )); // the output of the command in the console diff --git a/tests/VariantBaseCommandTest.php b/tests/VariantBaseCommandTest.php new file mode 100644 index 00000000..c56e0cd9 --- /dev/null +++ b/tests/VariantBaseCommandTest.php @@ -0,0 +1,101 @@ +application = new Application(); + $this->application->setVersion('3.0.0'); + $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + + $configuration = new ConfigurationService($this->application, $logger); + $method_factory = new MethodFactory($configuration, $logger); + $method_factory->addMethod(new FilesMethod($logger)); + $method_factory->addMethod(new ScriptMethod($logger)); + + $configuration->readConfiguration(getcwd() . '/assets/variants-base-command-tests/fabfile.yaml'); + + $this->application->add(new ScriptCommand($configuration, $method_factory)); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Could not find variants for `testMissingVariants` in `blueprints` + */ + public function testNoVariants() + { + $command = $this->application->find('script'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--config' => 'testMissingVariants', + '--variants' => 'all', + )); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Could not find variants `x`, `y`, `z` in `blueprints` + */ + public function testUnavailableVariants() + { + $command = $this->application->find('script'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--config' => 'test', + '--variants' => 'a,b,c,x,y,z', + )); + } + + /** + * @group docker + */ + public function testAllVariants() + { + $executable = realpath(getcwd() . '/../bin/phab'); + putenv('PHABALICIOUS_EXECUTABLE=' . $executable); + + $command = $this->application->find('script'); + $commandTester = new CommandTester($command); + $commandTester->execute(array( + 'command' => $command->getName(), + '--config' => 'test', + '--variants' => 'all', + '--force' => 1, + 'script' => 'test' + )); + + $output = $commandTester->getDisplay(); + $this->assertContains('--blueprint a', $output); + $this->assertContains('--blueprint b', $output); + $this->assertContains('--blueprint c', $output); + + $this->assertContains('XX-test-a-XX', $output); + $this->assertContains('XX-test-b-XX', $output); + $this->assertContains('XX-test-c-XX', $output); + } +} diff --git a/tests/assets/variants-base-command-tests/.fabfile.yaml b/tests/assets/variants-base-command-tests/.fabfile.yaml new file mode 100644 index 00000000..4013a0e1 --- /dev/null +++ b/tests/assets/variants-base-command-tests/.fabfile.yaml @@ -0,0 +1,37 @@ +name: variants-tests + +requires: 2.0.0 + +needs: + - script + + + +hosts: + testMissingVariants: + rootFolder: "%fabfile.path%" + + test: + scripts: + test: + - echo "XX-%host.configName%-XX" + - sleep $[ ( $RANDOM % 10 ) + 1 ]s + blueprint: + inheritsFrom: test + configName: test-%slug% + + + + +blueprints: + - configName: test + variants: + - a + - b + - c + - d + - e + - f + - g + - h +