From 098ec712bf219ce4bfa138d216ac3051748c496e Mon Sep 17 00:00:00 2001 From: Dane Powell Date: Thu, 18 May 2023 12:07:15 -0700 Subject: [PATCH] CLI-1055: [push:files] support local multisites (#1502) * CLI-1055: [push:files] support local multisites * Fix tests * kill mutants --- src/Command/Pull/PullCommandBase.php | 109 ++++++------------ src/Command/Push/PushFilesCommand.php | 48 ++------ tests/phpunit/src/CommandTestBase.php | 13 +-- .../Commands/Pull/PullFilesCommandTest.php | 10 +- .../Commands/Push/PushFilesCommandTest.php | 20 ++-- 5 files changed, 63 insertions(+), 137 deletions(-) diff --git a/src/Command/Pull/PullCommandBase.php b/src/Command/Pull/PullCommandBase.php index 216134e6f..13a553f5e 100644 --- a/src/Command/Pull/PullCommandBase.php +++ b/src/Command/Pull/PullCommandBase.php @@ -22,6 +22,7 @@ use GuzzleHttp\TransferStats; use Psr\Http\Message\UriInterface; use React\EventLoop\Loop; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; @@ -43,12 +44,20 @@ abstract class PullCommandBase extends CommandBase { private UriInterface $backupDownloadUrl; - /** - * @param $environment - * @param \AcquiaCloudApi\Response\DatabaseResponse $database - * @param $backupResponse - * @return string - */ + protected function getCloudFilesDir(EnvironmentResponse $chosenEnvironment, string $site): string { + $sitegroup = self::getSiteGroupFromSshUrl($chosenEnvironment->sshUrl); + if ($this->isAcsfEnv($chosenEnvironment)) { + return '/mnt/files/' . $sitegroup . '.' . $chosenEnvironment->name . '/sites/g/files/' . $site . '/files'; + } + else { + return $this->getCloudSitesPath($chosenEnvironment, $sitegroup) . "/$site/files"; + } + } + + protected function getLocalFilesDir(string $site): string { + return $this->dir . '/docroot/sites/' . $site . '/files'; + } + public static function getBackupPath($environment, DatabaseResponse $database, $backupResponse): string { // Databases have a machine name not exposed via the API; we can only // approximately reconstruct it and match the filename you'd get downloading @@ -73,11 +82,8 @@ protected function initialize(InputInterface $input, OutputInterface $output): v $this->checklist = new Checklist($output); } - /** - * @return int 0 if everything went fine, or an exit code - */ protected function execute(InputInterface $input, OutputInterface $output): int { - return 0; + return Command::SUCCESS; } protected function pullCode(InputInterface $input, OutputInterface $output): void { @@ -121,15 +127,15 @@ protected function pullDatabase(InputInterface $input, OutputInterface $output, $this->printDatabaseBackupInfo($backupResponse, $sourceEnvironment); } - $this->checklist->addItem("Downloading {$database->name} database copy from the Cloud Platform"); + $this->checklist->addItem("Downloading $database->name database copy from the Cloud Platform"); $localFilepath = $this->downloadDatabaseBackup($sourceEnvironment, $database, $backupResponse, $this->getOutputCallback($output, $this->checklist)); $this->checklist->completePreviousItem(); if ($noImport) { - $this->io->success("{$database->name} database backup downloaded to $localFilepath"); + $this->io->success("$database->name database backup downloaded to $localFilepath"); } else { - $this->checklist->addItem("Importing {$database->name} database download"); + $this->checklist->addItem("Importing $database->name database download"); $this->importRemoteDatabase($database, $localFilepath, $this->getOutputCallback($output, $this->checklist)); $this->checklist->completePreviousItem(); } @@ -260,11 +266,6 @@ private function getBackupDownloadUrl(): ?UriInterface { return $this->backupDownloadUrl ?? NULL; } - /** - * @param $totalBytes - * @param $downloadedBytes - * @param $progress - */ public static function displayDownloadProgress($totalBytes, $downloadedBytes, &$progress, OutputInterface $output): void { if ($totalBytes > 0 && is_null($progress)) { $progress = new ProgressBar($output, $totalBytes); @@ -337,9 +338,6 @@ private function connectToLocalDatabase(string $dbHost, string $dbUser, string $ } } - /** - * @param callable|null $outputCallback - */ private function dropLocalDatabase(string $dbHost, string $dbUser, string $dbName, string $dbPassword, callable $outputCallback = NULL): void { if ($outputCallback) { $outputCallback('out', "Dropping database $dbName"); @@ -360,9 +358,6 @@ private function dropLocalDatabase(string $dbHost, string $dbUser, string $dbNam } } - /** - * @param callable|null $outputCallback - */ private function createLocalDatabase(string $dbHost, string $dbUser, string $dbName, string $dbPassword, callable $outputCallback = NULL): void { if ($outputCallback) { $outputCallback('out', "Creating new empty database $dbName"); @@ -429,9 +424,6 @@ protected function getLocalGitCommitHash(): string { return trim($process->getOutput()); } - /** - * @param $acquiaCloudClient - */ private function promptChooseEnvironment($acquiaCloudClient, string $applicationUuid, bool $allowProduction = FALSE): EnvironmentResponse { $environmentResource = new Environments($acquiaCloudClient); $applicationEnvironments = iterator_to_array($environmentResource->getAll($applicationUuid)); @@ -443,7 +435,7 @@ private function promptChooseEnvironment($acquiaCloudClient, string $application $applicationEnvironments = array_values($applicationEnvironments); continue; } - $choices[] = "{$environment->label}, {$environment->name} (vcs: {$environment->vcs->path})"; + $choices[] = "$environment->label, $environment->name (vcs: {$environment->vcs->path})"; } $chosenEnvironmentLabel = $this->io->choice('Choose a Cloud Platform environment', $choices, $choices[0]); $chosenEnvironmentIndex = array_search($chosenEnvironmentLabel, $choices, TRUE); @@ -451,12 +443,6 @@ private function promptChooseEnvironment($acquiaCloudClient, string $application return $applicationEnvironments[$chosenEnvironmentIndex]; } - /** - * @param \AcquiaCloudApi\Response\EnvironmentResponse $cloudEnvironment - * @param \AcquiaCloudApi\Response\DatabasesResponse $environmentDatabases - * @param bool $multipleDbs - * @return DatabaseResponse[] - */ private function promptChooseDatabases( EnvironmentResponse $cloudEnvironment, DatabasesResponse $environmentDatabases, @@ -514,9 +500,6 @@ private function promptChooseDatabases( return [$environmentDatabases[$chosenDatabaseIndex]]; } - /** - * @param callable|null $outputCallback - */ protected function runComposerScripts(callable $outputCallback = NULL): void { if (file_exists($this->dir . '/composer.json') && $this->localMachineHelper->commandExists('composer')) { $this->checklist->addItem("Installing Composer dependencies"); @@ -528,9 +511,6 @@ protected function runComposerScripts(callable $outputCallback = NULL): void { } } - /** - * @param $environment - */ private function determineSite($environment, InputInterface $input): mixed { if (isset($this->site)) { return $this->site; @@ -551,22 +531,8 @@ private function determineSite($environment, InputInterface $input): mixed { return $site; } - /** - * @param $chosenEnvironment - * @param \Closure|null $outputCallback - */ - private function rsyncFilesFromCloud($chosenEnvironment, Closure $outputCallback = NULL, string $site): void { - $sitegroup = self::getSiteGroupFromSshUrl($chosenEnvironment->sshUrl); - if ($this->isAcsfEnv($chosenEnvironment)) { - $sourceDir = '/mnt/files/' . $sitegroup . '.' . $chosenEnvironment->name . '/sites/g/files/' . $site . '/files'; - $destination = $this->dir . '/docroot/sites/' . $site . '/'; - } - else { - $sourceDir = $this->getCloudSitesPath($chosenEnvironment, $sitegroup) . "/$site/files/"; - $destination = $this->dir . '/docroot/sites/' . $site . '/files'; - } + protected function rsyncFiles(string $sourceDir, string $destinationDir, ?callable $outputCallback): void { $this->localMachineHelper->checkRequiredBinariesExist(['rsync']); - $this->localMachineHelper->getFilesystem()->mkdir($destination); $command = [ 'rsync', // -a archive mode; same as -rlptgoD. @@ -577,15 +543,23 @@ private function rsyncFilesFromCloud($chosenEnvironment, Closure $outputCallback // -e specify the remote shell to use. '-avPhze', 'ssh -o StrictHostKeyChecking=no', - $chosenEnvironment->sshUrl . ':' . $sourceDir, - $destination, + $sourceDir . '/', + $destinationDir, ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, FALSE, 60 * 60); + $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to sync files from Cloud. {message}', ['message' => $process->getErrorOutput()]); + throw new AcquiaCliException('Unable to sync files. {message}', ['message' => $process->getErrorOutput()]); } } + private function rsyncFilesFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback, string $site): void { + $sourceDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site); + $destinationDir = $this->getLocalFilesDir($site); + $this->localMachineHelper->getFilesystem()->mkdir($destinationDir); + + $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback); + } + /** * @param \AcquiaCloudApi\Connector\Client $acquiaCloudClient * @param \AcquiaCloudApi\Response\EnvironmentResponse $chosenEnvironment @@ -677,7 +651,7 @@ protected function determineEnvironment(InputInterface $input, OutputInterface $ $acquiaCloudClient = $this->cloudApiClientService->getClient(); $chosenEnvironment = $this->promptChooseEnvironment($acquiaCloudClient, $cloudApplicationUuid, $allowProduction); } - $this->logger->debug("Using environment {$chosenEnvironment->label} {$chosenEnvironment->uuid}"); + $this->logger->debug("Using environment $chosenEnvironment->label $chosenEnvironment->uuid"); $this->sourceEnvironment = $chosenEnvironment; @@ -699,7 +673,7 @@ protected function matchIdePhpVersion( EnvironmentResponse $chosenEnvironment ): void { if (AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !$this->environmentPhpVersionMatches($chosenEnvironment)) { - $answer = $this->io->confirm("Would you like to change the PHP version on this IDE to match the PHP version on the {$chosenEnvironment->label} ({$chosenEnvironment->configuration->php->version}) environment?", FALSE); + $answer = $this->io->confirm("Would you like to change the PHP version on this IDE to match the PHP version on the $chosenEnvironment->label ({$chosenEnvironment->configuration->php->version}) environment?", FALSE); if ($answer) { $command = $this->getApplication()->find('ide:php-version'); $command->run(new ArrayInput(['command' => 'ide:php-version', 'version' => $chosenEnvironment->configuration->php->version]), @@ -708,17 +682,11 @@ protected function matchIdePhpVersion( } } - /** - * @param $environment - */ private function environmentPhpVersionMatches($environment): bool { $currentPhpVersion = $this->getIdePhpVersion(); return $environment->configuration->php->version === $currentPhpVersion; } - /** - * @param $input - */ protected function executeAllScripts($input, Closure $outputCallback): void { $this->setDirAndRequireProjectCwd($input); $this->runComposerScripts($outputCallback); @@ -829,8 +797,8 @@ private function printDatabaseBackupInfo( $dateFormatted = date("D M j G:i:s T Y", strtotime($backupResponse->completedAt)); $webLink = "https://cloud.acquia.com/a/environments/{$sourceEnvironment->uuid}/databases"; $messages = [ - "Using a database backup that is $hoursInterval hours old. Backup #{$backupResponse->id} was created at {$dateFormatted}.", - "You can view your backups here: {$webLink}", + "Using a database backup that is $hoursInterval hours old. Backup #$backupResponse->id was created at {$dateFormatted}.", + "You can view your backups here: $webLink", "To generate a new backup, re-run this command with the --on-demand option.", ]; if ($hoursInterval > 24) { @@ -841,9 +809,6 @@ private function printDatabaseBackupInfo( } } - /** - * @param callable|null $outputCallback - */ private function importRemoteDatabase(DatabaseResponse $database, string $localFilepath, Closure $outputCallback = NULL): void { if ($database->flags->default) { // Easy case, import the default db into the default db. diff --git a/src/Command/Push/PushFilesCommand.php b/src/Command/Push/PushFilesCommand.php index d5528f10b..120474069 100644 --- a/src/Command/Push/PushFilesCommand.php +++ b/src/Command/Push/PushFilesCommand.php @@ -3,9 +3,10 @@ namespace Acquia\Cli\Command\Push; use Acquia\Cli\Command\Pull\PullCommandBase; -use Acquia\Cli\Exception\AcquiaCliException; use Acquia\Cli\Output\Checklist; use Acquia\DrupalEnvironmentDetector\AcquiaDrupalEnvironmentDetector; +use AcquiaCloudApi\Response\EnvironmentResponse; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -20,9 +21,6 @@ protected function configure(): void { ->setHidden(!AcquiaDrupalEnvironmentDetector::isAhIdeEnv() && !self::isLandoEnv()); } - /** - * @return int 0 if everything went fine, or an exit code - */ protected function execute(InputInterface $input, OutputInterface $output): int { $this->setDirAndRequireProjectCwd($input); $destinationEnvironment = $this->determineEnvironment($input, $output); @@ -35,9 +33,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $chosenSite = $this->promptChooseCloudSite($destinationEnvironment); } } - $answer = $this->io->confirm("Overwrite the public files directory on {$destinationEnvironment->name} with a copy of the files from the current machine?"); + $answer = $this->io->confirm("Overwrite the public files directory on $destinationEnvironment->name with a copy of the files from the current machine?"); if (!$answer) { - return 0; + return Command::SUCCESS; } $this->checklist = new Checklist($output); @@ -45,42 +43,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->rsyncFilesToCloud($destinationEnvironment, $this->getOutputCallback($output, $this->checklist), $chosenSite); $this->checklist->completePreviousItem(); - return 0; + return Command::SUCCESS; } - /** - * @param $chosenEnvironment - * @param callable|null $outputCallback - * @param string|null $site - */ - private function rsyncFilesToCloud($chosenEnvironment, callable $outputCallback = NULL, string $site = NULL): void { - $source = $this->dir . '/docroot/sites/default/files/'; - $sitegroup = self::getSiteGroupFromSshUrl($chosenEnvironment->sshUrl); + private function rsyncFilesToCloud(EnvironmentResponse $chosenEnvironment, callable $outputCallback = NULL, string $site = NULL): void { + $sourceDir = $this->getLocalFilesDir($site); + $destinationDir = $chosenEnvironment->sshUrl . ':' . $this->getCloudFilesDir($chosenEnvironment, $site); - if ($this->isAcsfEnv($chosenEnvironment)) { - $destDir = '/mnt/files/' . $sitegroup . '.' . $chosenEnvironment->name . '/sites/g/files/' . $site . '/files'; - } - else { - $destDir = '/mnt/files/' . $sitegroup . '.' . $chosenEnvironment->name . '/sites/' . $site . '/files'; - } - $this->localMachineHelper->checkRequiredBinariesExist(['rsync']); - $command = [ - 'rsync', - // -a archive mode; same as -rlptgoD. - // -z compress file data during the transfer. - // -v increase verbosity. - // -P show progress during transfer. - // -h output numbers in a human-readable format. - // -e specify the remote shell to use. - '-avPhze', - 'ssh -o StrictHostKeyChecking=no', - $source, - $chosenEnvironment->sshUrl . ':' . $destDir, - ]; - $process = $this->localMachineHelper->execute($command, $outputCallback, NULL, ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL)); - if (!$process->isSuccessful()) { - throw new AcquiaCliException('Unable to sync files to Cloud. {message}', ['message' => $process->getErrorOutput()]); - } + $this->rsyncFiles($sourceDir, $destinationDir, $outputCallback); } } diff --git a/tests/phpunit/src/CommandTestBase.php b/tests/phpunit/src/CommandTestBase.php index eb3b187bc..f5bc22935 100644 --- a/tests/phpunit/src/CommandTestBase.php +++ b/tests/phpunit/src/CommandTestBase.php @@ -241,23 +241,18 @@ public function mockAcsfEnvironmentsRequest( return $environmentsResponse; } - /** - * @param $sshHelper - */ - protected function mockGetAcsfSites($sshHelper): void { + protected function mockGetAcsfSites($sshHelper): array { $acsfMultisiteFetchProcess = $this->mockProcess(); - $acsfMultisiteFetchProcess->getOutput()->willReturn(file_get_contents(Path::join($this->realFixtureDir, - '/multisite-config.json')))->shouldBeCalled(); + $multisiteConfig = file_get_contents(Path::join($this->realFixtureDir, '/multisite-config.json')); + $acsfMultisiteFetchProcess->getOutput()->willReturn($multisiteConfig)->shouldBeCalled(); $sshHelper->executeCommand( Argument::type('object'), ['cat', '/var/www/site-php/profserv2.dev/multisite-config.json'], FALSE )->willReturn($acsfMultisiteFetchProcess->reveal())->shouldBeCalled(); + return json_decode($multisiteConfig, TRUE); } - /** - * @param $sshHelper - */ protected function mockGetCloudSites($sshHelper, $environment): void { $cloudMultisiteFetchProcess = $this->mockProcess(); $cloudMultisiteFetchProcess->getOutput()->willReturn("\nbar\ndefault\nfoo\n")->shouldBeCalled(); diff --git a/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php b/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php index 32d2edfb6..f22329cb8 100644 --- a/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php +++ b/tests/phpunit/src/Commands/Pull/PullFilesCommandTest.php @@ -5,14 +5,12 @@ use Acquia\Cli\Command\CommandBase; use Acquia\Cli\Command\Pull\PullFilesCommand; use Acquia\Cli\Exception\AcquiaCliException; +use Acquia\Cli\Helpers\LocalMachineHelper; use Acquia\Cli\Tests\Commands\Ide\IdeHelper; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Console\Command\Command; -/** - * @property \Acquia\Cli\Command\Pull\PullFilesCommand $command - */ class PullFilesCommandTest extends PullCommandTestBase { protected function createCommand(): Command { @@ -28,7 +26,7 @@ public function testRefreshAcsfFiles(): void { $this->mockGetAcsfSites($sshHelper); $localMachineHelper = $this->mockLocalMachineHelper(); $this->mockGetFilesystem($localMachineHelper); - $this->mockExecuteRsync($localMachineHelper, $selectedEnvironment, '/mnt/files/profserv2.dev/sites/g/files/jxr5000596dev/files', $this->projectDir . '/docroot/sites/jxr5000596dev/'); + $this->mockExecuteRsync($localMachineHelper, $selectedEnvironment, '/mnt/files/profserv2.dev/sites/g/files/jxr5000596dev/files/', $this->projectDir . '/docroot/sites/jxr5000596dev/files'); $this->command->localMachineHelper = $localMachineHelper->reveal(); $this->command->sshHelper = $sshHelper->reveal(); @@ -109,7 +107,7 @@ public function testInvalidCwd(): void { * @param $environment */ protected function mockExecuteRsync( - ObjectProphecy $localMachineHelper, + LocalMachineHelper|ObjectProphecy $localMachineHelper, $environment, string $sourceDir, string $destinationDir @@ -123,7 +121,7 @@ protected function mockExecuteRsync( $environment->ssh_url . ':' . $sourceDir, $destinationDir, ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, FALSE, 60 * 60) + $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE) ->willReturn($process->reveal()) ->shouldBeCalled(); } diff --git a/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php b/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php index e2168e4e5..71fb84369 100644 --- a/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php +++ b/tests/phpunit/src/Commands/Push/PushFilesCommandTest.php @@ -9,9 +9,6 @@ use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Console\Command\Command; -/** - * @property \Acquia\Cli\Command\Push\PushFilesCommand $command - */ class PushFilesCommandTest extends CommandTestBase { protected function createCommand(): Command { @@ -23,10 +20,10 @@ public function testPushFilesAcsf(): void { $this->mockApplicationRequest(); $this->mockAcsfEnvironmentsRequest($applicationsResponse); $sshHelper = $this->mockSshHelper(); - $this->mockGetAcsfSites($sshHelper); + $multisiteConfig = $this->mockGetAcsfSites($sshHelper); $localMachineHelper = $this->mockLocalMachineHelper(); $process = $this->mockProcess(); - $this->mockExecuteAcsfRsync($localMachineHelper, $process); + $this->mockExecuteAcsfRsync($localMachineHelper, $process, reset($multisiteConfig['sites'])['name']); $this->command->localMachineHelper = $localMachineHelper->reveal(); $this->command->sshHelper = $sshHelper->reveal(); @@ -106,27 +103,28 @@ protected function mockExecuteCloudRsync( 'rsync', '-avPhze', 'ssh -o StrictHostKeyChecking=no', - $this->projectDir . '/docroot/sites/default/files/', + $this->projectDir . '/docroot/sites/bar/files/', $environment->ssh_url . ':/mnt/files/' . $sitegroup . '.' . $environment->name . '/sites/bar/files', ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE, NULL) + $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE) ->willReturn($process->reveal()) ->shouldBeCalled(); } protected function mockExecuteAcsfRsync( ObjectProphecy $localMachineHelper, - ObjectProphecy $process + ObjectProphecy $process, + string $site ): void { $localMachineHelper->checkRequiredBinariesExist(['rsync'])->shouldBeCalled(); $command = [ 'rsync', '-avPhze', 'ssh -o StrictHostKeyChecking=no', - $this->projectDir . '/docroot/sites/default/files/', - 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com:/mnt/files/profserv2.dev/sites/g/files/jxr5000596dev/files', + $this->projectDir . '/docroot/sites/' . $site . '/files/', + 'profserv2.01dev@profserv201dev.ssh.enterprise-g1.acquia-sites.com:/mnt/files/profserv2.dev/sites/g/files/' . $site . '/files', ]; - $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE, NULL) + $localMachineHelper->execute($command, Argument::type('callable'), NULL, TRUE) ->willReturn($process->reveal()) ->shouldBeCalled(); }