Skip to content

Commit

Permalink
CLI-1055: [push:files] support local multisites (#1502)
Browse files Browse the repository at this point in the history
* CLI-1055: [push:files] support local multisites

* Fix tests

* kill mutants
  • Loading branch information
danepowell authored May 18, 2023
1 parent 392464d commit 098ec71
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 137 deletions.
109 changes: 37 additions & 72 deletions src/Command/Pull/PullCommandBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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));
Expand All @@ -443,20 +435,14 @@ 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);

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,
Expand Down Expand Up @@ -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");
Expand All @@ -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;
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -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 <bg=cyan;options=bold>{$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 <bg=cyan;options=bold>$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]),
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
48 changes: 9 additions & 39 deletions src/Command/Push/PushFilesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -35,52 +33,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$chosenSite = $this->promptChooseCloudSite($destinationEnvironment);
}
}
$answer = $this->io->confirm("Overwrite the public files directory on <bg=cyan;options=bold>{$destinationEnvironment->name}</> with a copy of the files from the current machine?");
$answer = $this->io->confirm("Overwrite the public files directory on <bg=cyan;options=bold>$destinationEnvironment->name</> with a copy of the files from the current machine?");
if (!$answer) {
return 0;
return Command::SUCCESS;
}

$this->checklist = new Checklist($output);
$this->checklist->addItem('Pushing public files directory to remote machine');
$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);
}

}
13 changes: 4 additions & 9 deletions tests/phpunit/src/CommandTestBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 098ec71

Please sign in to comment.