diff --git a/ci/test/magento/deploy_brancher.php b/ci/test/magento/deploy_brancher.php index 65581a1..947bf1b 100644 --- a/ci/test/magento/deploy_brancher.php +++ b/ci/test/magento/deploy_brancher.php @@ -18,6 +18,7 @@ ]); $productionStage = $configuration->addStage('test', 'banaan.store'); -$productionStage->addBrancherServer('hndeployintegr8'); +$productionStage->addBrancherServer('hndeployintegr8') + ->setLabels(['gitref='.\getenv('GITHUB_SHA') ?: 'unknown']); return $configuration; diff --git a/ci/test/run-brancher.sh b/ci/test/run-brancher.sh index 219c7ea..35e1437 100755 --- a/ci/test/run-brancher.sh +++ b/ci/test/run-brancher.sh @@ -57,3 +57,26 @@ $DP jq .brancher_hypernodes[0] deployment-report.json -r -e # cleanup data $DP hypernode-deploy cleanup -vvv + +rm -f deployment-report.json + +# Now do a test deploy again to deploy to a brancher node and clean it up by hnapi and labels matching +$DP hypernode-deploy deploy test -f /web/deploy.php -vvv + +$DP ls -l +$DP test -f deployment-report.json +$DP jq . deployment-report.json +$DP jq .version deployment-report.json -r -e +$DP jq .stage deployment-report.json -r -e +$DP jq .hostnames[0] deployment-report.json -r -e +$DP jq .brancher_hypernodes[0] deployment-report.json -r -e + +# Remove deployment report to make sure we can clean up using hnapi and labels matching +BRANCHER_INSTANCE=$($DP jq .brancher_hypernodes[0] deployment-report.json -r -e) +$DP rm -f deployment-report.json + +# cleanup data +$DP hypernode-deploy cleanup test -vvv | tee cleanup.log + +# Run tests on cleanup +grep "Stopping brancher Hypernode ${BRANCHER_INSTANCE}..." cleanup.log diff --git a/composer.lock b/composer.lock index 32afd88..22d36eb 100644 --- a/composer.lock +++ b/composer.lock @@ -729,16 +729,16 @@ }, { "name": "hypernode/api-client", - "version": "0.2.0", + "version": "0.2.1", "source": { "type": "git", "url": "https://github.com/ByteInternet/hypernode-api-php.git", - "reference": "9a93ee94be06a2265cf7fb6eed75b50a376adc9b" + "reference": "5fc348028551b5c6d2bbf1c1894f2cc3d59230e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ByteInternet/hypernode-api-php/zipball/9a93ee94be06a2265cf7fb6eed75b50a376adc9b", - "reference": "9a93ee94be06a2265cf7fb6eed75b50a376adc9b", + "url": "https://api.github.com/repos/ByteInternet/hypernode-api-php/zipball/5fc348028551b5c6d2bbf1c1894f2cc3d59230e9", + "reference": "5fc348028551b5c6d2bbf1c1894f2cc3d59230e9", "shasum": "" }, "require": { @@ -774,9 +774,9 @@ "description": "Hypernode API Client for PHP", "support": { "issues": "https://github.com/ByteInternet/hypernode-api-php/issues", - "source": "https://github.com/ByteInternet/hypernode-api-php/tree/0.2.0" + "source": "https://github.com/ByteInternet/hypernode-api-php/tree/0.2.1" }, - "time": "2022-11-23T11:15:44+00:00" + "time": "2022-11-25T14:45:54+00:00" }, { "name": "hypernode/deploy-configuration", diff --git a/src/Brancher/BrancherHypernodeManager.php b/src/Brancher/BrancherHypernodeManager.php index 4d7572f..d986204 100644 --- a/src/Brancher/BrancherHypernodeManager.php +++ b/src/Brancher/BrancherHypernodeManager.php @@ -22,6 +22,40 @@ public function __construct(LoggerInterface $log) $this->hypernodeClient = HypernodeClientFactory::create(getenv('HYPERNODE_API_TOKEN') ?: ''); } + /** + * Query brancher instances for given Hypernode and return the Brancher instance names. + * + * @param string $hypernode The parent hypernode to query the Brancher instances from + * @param string[] $labels Labels to match against, may be empty + * @return string[] The found Brancher instance names + */ + public function queryBrancherHypernodes(string $hypernode, array $labels = []): array + { + $result = []; + + $hypernodes = $this->hypernodeClient->app->getList([ + 'parent' => $hypernode, + 'type' => 'brancher', + 'destroyed' => 'False', + ]); + foreach ($hypernodes as $brancher) { + $match = true; + + foreach ($labels as $label) { + if (!in_array($label, $brancher->labels)) { + $match = false; + break; + } + } + + if ($match) { + $result[] = $brancher->name; + } + } + + return $result; + } + /** * Create brancher Hypernode instance for given Hypernode. * @@ -82,7 +116,7 @@ public function waitForAvailability(string $brancherHypernode, int $timeout = 90 } elseif ($timeElapsed < $allowedErrorWindow) { // Sometimes we get an error where the logbook is not yet available, but it will be soon. // We allow a small window for this to happen, and then we throw an exception. - sprintf( + printf( 'Got an expected exception during the allowed error window of HTTP code %d, waiting for %s to become available', $e->getCode(), $brancherHypernode diff --git a/src/Command/Cleanup.php b/src/Command/Cleanup.php index fd0576b..6232b15 100644 --- a/src/Command/Cleanup.php +++ b/src/Command/Cleanup.php @@ -5,8 +5,14 @@ namespace Hypernode\Deploy\Command; use Hypernode\Deploy\Brancher\BrancherHypernodeManager; +use Hypernode\Deploy\ConfigurationLoader; +use Hypernode\Deploy\DeployerLoader; use Hypernode\Deploy\Report\ReportLoader; +use Hypernode\DeployConfiguration\BrancherServer; +use Hypernode\DeployConfiguration\Configuration; +use Hypernode\DeployConfiguration\Server; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Throwable; @@ -14,13 +20,21 @@ class Cleanup extends Command { private ReportLoader $reportLoader; + private DeployerLoader $deployerLoader; + private ConfigurationLoader $configurationLoader; private BrancherHypernodeManager $brancherHypernodeManager; - public function __construct(ReportLoader $reportLoader, BrancherHypernodeManager $brancherHypernodeManager) - { + public function __construct( + ReportLoader $reportLoader, + DeployerLoader $deployerLoader, + ConfigurationLoader $configurationLoader, + BrancherHypernodeManager $brancherHypernodeManager + ) { parent::__construct(); $this->reportLoader = $reportLoader; + $this->deployerLoader = $deployerLoader; + $this->configurationLoader = $configurationLoader; $this->brancherHypernodeManager = $brancherHypernodeManager; } @@ -31,6 +45,7 @@ protected function configure() $this->setDescription( 'Clean up any acquired resources during the deployment, like brancher Hypernodes.' ); + $this->addArgument('stage', InputArgument::OPTIONAL, 'Stage to cleanup'); } /** @@ -40,13 +55,46 @@ protected function execute(InputInterface $input, OutputInterface $output) { $report = $this->reportLoader->loadReport(); - if ($report === null) { - $output->writeln('No report found, skipping cleanup.'); - return 0; + if ($report) { + $this->brancherHypernodeManager->cancel(...$report->getBrancherHypernodes()); } - $this->brancherHypernodeManager->cancel(...$report->getBrancherHypernodes()); + /** @var string $stageName */ + $stageName = $input->getArgument('stage'); + if ($stageName) { + $this->deployerLoader->getOrCreateInstance($output); + $config = $this->configurationLoader->load($input->getOption('file') ?: 'deploy.php'); + $this->cancelByStage($stageName, $config); + } return 0; } + + /** + * Cancel brancher nodes by stage and their configured labels. + * + * @param string $stageName Stage to clean up + * @param Configuration $config Deployment configuration to read stages/servers from + * @return void + */ + private function cancelByStage(string $stageName, Configuration $config): void + { + foreach ($config->getStages() as $stage) { + if ($stage->getName() !== $stageName) { + continue; + } + foreach ($stage->getServers() as $server) { + if (!($server instanceof BrancherServer)) { + continue; + } + $labels = $server->getLabels(); + $hypernode = $server->getOptions()[Server::OPTION_HN_PARENT_APP]; + $brancherHypernodes = $this->brancherHypernodeManager->queryBrancherHypernodes( + $hypernode, + $labels + ); + $this->brancherHypernodeManager->cancel(...$brancherHypernodes); + } + } + } } diff --git a/src/Command/Deploy.php b/src/Command/Deploy.php index 9b6031f..2ec7c46 100644 --- a/src/Command/Deploy.php +++ b/src/Command/Deploy.php @@ -12,10 +12,7 @@ class Deploy extends Command { - /** - * @var DeployRunner - */ - private $deployRunner; + private DeployRunner $deployRunner; private ReportWriter $reportWriter; public function __construct(DeployRunner $deployRunner, ReportWriter $reportWriter) @@ -38,7 +35,13 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $result = $this->deployRunner->run($output, $input->getArgument('stage'), DeployRunner::TASK_DEPLOY, false, true); + $result = $this->deployRunner->run( + $output, + $input->getArgument('stage'), + DeployRunner::TASK_DEPLOY, + false, + true + ); if ($result === 0) { $this->reportWriter->write($this->deployRunner->getDeploymentReport()); diff --git a/src/ConfigurationLoader.php b/src/ConfigurationLoader.php new file mode 100644 index 0000000..8891c8c --- /dev/null +++ b/src/ConfigurationLoader.php @@ -0,0 +1,38 @@ +taskFactory = $taskFactory; $this->input = $input; $this->log = $log; $this->recipeLoader = $recipeLoader; + $this->deployerLoader = $deployerLoader; + $this->configurationLoader = $configurationLoader; $this->brancherHypernodeManager = $brancherHypernodeManager; } @@ -76,19 +76,10 @@ public function __construct( */ public function run(OutputInterface $output, string $stage, string $task, bool $configureBuildStage, bool $configureServers): int { - $console = new Application(); - $deployer = new Deployer($console); - $deployer['output'] = new OutputWatcher($output); - $deployer['input'] = new ArrayInput( - [], - new InputDefinition([ - new InputOption('limit'), - new InputOption('profile'), - ]) - ); + $deployer = $this->deployerLoader->getOrCreateInstance($output); try { - $this->initializeDeployer($deployer, $configureBuildStage, $configureServers, $stage); + $this->prepare($configureBuildStage, $configureServers, $stage); } catch (InvalidConfigurationException | ValidationException $e) { $output->write($e->getMessage()); return 1; @@ -98,22 +89,20 @@ public function run(OutputInterface $output, string $stage, string $task, bool $ } /** - * Initialize deployer settings + * Prepare deploy runner before running stage * * @throws Exception * @throws GracefulShutdownException * @throws InvalidConfigurationException * @throws Throwable */ - private function initializeDeployer( - Deployer $deployer, - bool $configureBuildStage, - bool $configureServers, - string $stage - ): void { + private function prepare(bool $configureBuildStage, bool $configureServers, string $stage): void + { $this->recipeLoader->load('common.php'); $tasks = $this->taskFactory->loadAll(); - $config = $this->getConfiguration($deployer); + $config = $this->configurationLoader->load( + $this->input->getOption('file') ?: 'deploy.php' + ); $config->setLogger($this->log); if ($configureBuildStage) { @@ -175,23 +164,6 @@ private function initializeConfigurableTask(ConfigurableTaskInterface $task, Con } } - /** - * @throws Exception - * @throws GracefulShutdownException - * @throws Throwable - */ - private function getConfiguration(Deployer $deployer): Configuration - { - try { - return $this->tryGetConfiguration(); - } catch (\Throwable $e) { - $this->log->warning(sprintf('Failed to initialize deploy.php configuration file: %s', $e->getMessage())); - $this->tryComposerInstall($deployer); - $this->initializeAppAutoloader(); - return $this->tryGetConfiguration(); - } - } - private function configureServers(Configuration $config, string $stage): void { foreach ($config->getStages() as $configStage) { @@ -358,65 +330,6 @@ private function runStage(Deployer $deployer, string $stage, string $task = 'dep return $exitCode; } - private function tryGetConfiguration(): Configuration - { - $file = $this->input->getOption('file'); - if (!$file) { - $file = 'deploy.php'; - } - - if (!is_readable($file)) { - throw new \RuntimeException(sprintf('No %s file found in project root %s', $file, getcwd())); - } - - $configuration = \call_user_func(function () use ($file) { - return require $file; - }); - - if (!$configuration instanceof Configuration) { - throw new \RuntimeException( - sprintf('%s/deploy.php did not return object of type %s', getcwd(), Configuration::class) - ); - } - - return $configuration; - } - - /** - * @throws GracefulShutdownException - * @throws Throwable - * @throws Exception - */ - private function tryComposerInstall(Deployer $deployer): void - { - /** @psalm-suppress InvalidArgument deployer will have proper typing in 7.x */ - $host = localhost('composer-prepare'); - $host->set('labels', ['stage' => 'composer-prepare']); - $host->set('bin/php', 'php'); - - task('composer-prepare:install', function () { - run('composer install --ignore-platform-reqs --optimize-autoloader --no-dev'); - }); - - task('composer-prepare', [ - 'deploy:vendors:auth', - 'composer-prepare:install', - ]); - - $this->runStage($deployer, 'composer-prepare', 'composer-prepare'); - } - - /** - * Initialize autoloader of the application being deployed - */ - private function initializeAppAutoloader(): void - { - /** @psalm-suppress UndefinedConstant */ - if (file_exists(WORKING_DIR . '/vendor/autoload.php')) { - require_once WORKING_DIR . '/vendor/autoload.php'; - } - } - public function getDeploymentReport() { return new Report\Report( diff --git a/src/DeployerLoader.php b/src/DeployerLoader.php new file mode 100644 index 0000000..976dec0 --- /dev/null +++ b/src/DeployerLoader.php @@ -0,0 +1,38 @@ +deployer) { + return $this->deployer; + } + + $console = new Application(); + $this->deployer = new Deployer($console); + $this->deployer['output'] = new OutputWatcher($output); + $this->deployer['input'] = new ArrayInput( + [], + new InputDefinition([ + new InputOption('limit'), + new InputOption('profile'), + ]) + ); + + return $this->deployer; + } +}