Skip to content

Commit

Permalink
CLI-1330: TypeError for SSH commands on Node environments (#1734)
Browse files Browse the repository at this point in the history
* CLI-1330: TypeError for SSH commands on Node environments

* trigger codecov

* Don't allow node environments

* kill mutant

* kill mutant

* kill mutants

* kill mutant

* kill mutant

* kill mutant

* determineEnvironment flip parameter

* Kill mutants

* kill mutant
  • Loading branch information
danepowell authored May 8, 2024
1 parent 1c8febf commit 012ac02
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 107 deletions.
35 changes: 24 additions & 11 deletions src/Command/CommandBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ private function promptChooseDatabases(
return [$environmentDatabases[$chosenDatabaseIndex]];
}

protected function determineEnvironment(InputInterface $input, OutputInterface $output, bool $allowProduction = FALSE): array|string|EnvironmentResponse {
protected function determineEnvironment(InputInterface $input, OutputInterface $output, bool $allowProduction = FALSE, bool $allowNode = FALSE): array|string|EnvironmentResponse {
if ($input->getArgument('environmentId')) {
$environmentId = $input->getArgument('environmentId');
$chosenEnvironment = $this->getCloudEnvironment($environmentId);
Expand All @@ -584,27 +584,32 @@ protected function determineEnvironment(InputInterface $input, OutputInterface $
$cloudApplication = $this->getCloudApplication($cloudApplicationUuid);
$output->writeln('Using Cloud Application <options=bold>' . $cloudApplication->name . '</>');
$acquiaCloudClient = $this->cloudApiClientService->getClient();
$chosenEnvironment = $this->promptChooseEnvironmentConsiderProd($acquiaCloudClient, $cloudApplicationUuid, $allowProduction);
$chosenEnvironment = $this->promptChooseEnvironmentConsiderProd($acquiaCloudClient, $cloudApplicationUuid, $allowProduction, $allowNode);
}
$this->logger->debug("Using environment $chosenEnvironment->label $chosenEnvironment->uuid");

return $chosenEnvironment;
}

// Todo: obviously combine this with promptChooseEnvironment.
private function promptChooseEnvironmentConsiderProd(Client $acquiaCloudClient, string $applicationUuid, bool $allowProduction = FALSE): EnvironmentResponse {
private function promptChooseEnvironmentConsiderProd(Client $acquiaCloudClient, string $applicationUuid, bool $allowProduction, bool $allowNode): EnvironmentResponse {
$environmentResource = new Environments($acquiaCloudClient);
$applicationEnvironments = iterator_to_array($environmentResource->getAll($applicationUuid));
$choices = [];
foreach ($applicationEnvironments as $key => $environment) {
if (!$allowProduction && $environment->flags->production) {
$productionNotAllowed = !$allowProduction && $environment->flags->production;
$nodeNotAllowed = !$allowNode && $environment->type === 'node';
if ($productionNotAllowed || $nodeNotAllowed) {
unset($applicationEnvironments[$key]);
// Re-index array so keys match those in $choices.
$applicationEnvironments = array_values($applicationEnvironments);
continue;
}
$choices[] = "$environment->label, $environment->name (vcs: {$environment->vcs->path})";
}
if (count($choices) === 0) {
throw new AcquiaCliException('No compatible environments found');
}
$chosenEnvironmentLabel = $this->io->choice('Choose a Cloud Platform environment', $choices, $choices[0]);
$chosenEnvironmentIndex = array_search($chosenEnvironmentLabel, $choices, TRUE);

Expand Down Expand Up @@ -1330,10 +1335,10 @@ protected function isAcsfEnv(mixed $cloudEnvironment): bool {
/**
* @return array<mixed>
*/
protected function getAcsfSites(EnvironmentResponse $cloudEnvironment): array {
private function getAcsfSites(EnvironmentResponse $cloudEnvironment): array {
$envAlias = self::getEnvironmentAlias($cloudEnvironment);
$command = ['cat', "/var/www/site-php/$envAlias/multisite-config.json"];
$process = $this->sshHelper->executeCommand($cloudEnvironment, $command, FALSE);
$process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, FALSE);
if ($process->isSuccessful()) {
return json_decode($process->getOutput(), TRUE, 512, JSON_THROW_ON_ERROR);
}
Expand All @@ -1346,7 +1351,7 @@ protected function getAcsfSites(EnvironmentResponse $cloudEnvironment): array {
private function getCloudSites(EnvironmentResponse $cloudEnvironment): array {
$sitegroup = self::getSitegroup($cloudEnvironment);
$command = ['ls', $this->getCloudSitesPath($cloudEnvironment, $sitegroup)];
$process = $this->sshHelper->executeCommand($cloudEnvironment, $command, FALSE);
$process = $this->sshHelper->executeCommand($cloudEnvironment->sshUrl, $command, FALSE);
$sites = array_filter(explode("\n", trim($process->getOutput())));
if ($process->isSuccessful() && $sites) {
return $sites;
Expand Down Expand Up @@ -1682,17 +1687,17 @@ private function getAnyAhEnvironment(string $cloudAppUuid, callable $filter): En
* Get the first non-prod environment for a given Cloud application.
*/
protected function getAnyNonProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false {
return $this->getAnyAhEnvironment($cloudAppUuid, function (mixed $environment) {
return !$environment->flags->production;
return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) {
return !$environment->flags->production && $environment->type === 'drupal';
});
}

/**
* Get the first prod environment for a given Cloud application.
*/
protected function getAnyProdAhEnvironment(string $cloudAppUuid): EnvironmentResponse|false {
return $this->getAnyAhEnvironment($cloudAppUuid, function (mixed $environment) {
return $environment->flags->production;
return $this->getAnyAhEnvironment($cloudAppUuid, function (EnvironmentResponse $environment) {
return $environment->flags->production && $environment->type === 'drupal';
});
}

Expand Down Expand Up @@ -1843,4 +1848,12 @@ protected function validatePhpVersion(string $version): string {
return $version;
}

protected function promptChooseDrupalSite(EnvironmentResponse $environment): string {
if ($this->isAcsfEnv($environment)) {
return $this->promptChooseAcsfSite($environment);
}

return $this->promptChooseCloudSite($environment);
}

}
2 changes: 1 addition & 1 deletion src/Command/Env/EnvCertCreateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ protected function configure(): void {

protected function execute(InputInterface $input, OutputInterface $output): int {
$acquiaCloudClient = $this->cloudApiClientService->getClient();
$environment = $this->determineEnvironment($input, $output);
$environment = $this->determineEnvironment($input, $output, TRUE, TRUE);
$certificate = $input->getArgument('certificate');
$privateKey = $input->getArgument('private-key');
$label = $this->determineOption('label');
Expand Down
12 changes: 3 additions & 9 deletions src/Command/Pull/PullCommandBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ private function importDatabaseDump(string $localDumpFilepath, string $dbHost, s
}
}

private function determineSite(string|EnvironmentResponse|array $environment, InputInterface $input): mixed {
private function determineSite(string|EnvironmentResponse|array $environment, InputInterface $input): string {
if (isset($this->site)) {
return $this->site;
}
Expand All @@ -423,15 +423,9 @@ private function determineSite(string|EnvironmentResponse|array $environment, In
return $input->getArgument('site');
}

if ($this->isAcsfEnv($environment)) {
$site = $this->promptChooseAcsfSite($environment);
}
else {
$site = $this->promptChooseCloudSite($environment);
}
$this->site = $site;
$this->site = $this->promptChooseDrupalSite($environment);

return $site;
return $this->site;
}

private function rsyncFilesFromCloud(EnvironmentResponse $chosenEnvironment, Closure $outputCallback, string $site): void {
Expand Down
2 changes: 1 addition & 1 deletion src/Command/Push/PushDatabaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private function uploadDatabaseDump(
private function importDatabaseDumpOnRemote(EnvironmentResponse $environment, string $remoteDumpFilepath, DatabaseResponse $database): void {
$this->logger->debug("Importing $remoteDumpFilepath to MySQL on remote machine");
$command = "pv $remoteDumpFilepath --bytes --rate | gunzip | MYSQL_PWD={$database->password} mysql --host={$this->getHostFromDatabaseResponse($environment, $database)} --user={$database->user_name} {$this->getNameFromDatabaseResponse($database)}";
$process = $this->sshHelper->executeCommand($environment, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
$process = $this->sshHelper->executeCommand($environment->sshUrl, [$command], ($this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL));
if (!$process->isSuccessful()) {
throw new AcquiaCliException('Unable to import database on remote machine. {message}', ['message' => $process->getErrorOutput()]);
}
Expand Down
7 changes: 1 addition & 6 deletions src/Command/Push/PushFilesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$destinationEnvironment = $this->determineEnvironment($input, $output);
$chosenSite = $input->getArgument('site');
if (!$chosenSite) {
if ($this->isAcsfEnv($destinationEnvironment)) {
$chosenSite = $this->promptChooseAcsfSite($destinationEnvironment);
}
else {
$chosenSite = $this->promptChooseCloudSite($destinationEnvironment);
}
$chosenSite = $this->promptChooseDrupalSite($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?");
if (!$answer) {
Expand Down
2 changes: 1 addition & 1 deletion src/Command/Remote/DrushCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int
implode(' ', $drushArguments),
];

return $this->sshHelper->executeCommand($environment, $drushCommandArguments)->getExitCode();
return $this->sshHelper->executeCommand($environment->sshUrl, $drushCommandArguments)->getExitCode();
}

}
2 changes: 1 addition & 1 deletion src/Command/Remote/SshCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output): ?int
$sshCommand[] = implode(' ', $arguments['ssh_command']);
}
$sshCommand = (array) implode('; ', $sshCommand);
return $this->sshHelper->executeCommand($environment, $sshCommand)->getExitCode();
return $this->sshHelper->executeCommand($environment->sshUrl, $sshCommand)->getExitCode();
}

}
4 changes: 2 additions & 2 deletions src/Command/Ssh/SshKeyCommandBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,12 @@ private function checkPermissions(array $userPerms, string $cloudAppUuid, Output
break;
case 'add ssh key to non-prod':
if ($nonProdEnv = $this->getAnyNonProdAhEnvironment($cloudAppUuid)) {
$mappings['nonprod']['ssh_target'] = $nonProdEnv;
$mappings['nonprod']['ssh_target'] = $nonProdEnv->sshUrl;
}
break;
case 'add ssh key to prod':
if ($prodEnv = $this->getAnyProdAhEnvironment($cloudAppUuid)) {
$mappings['prod']['ssh_target'] = $prodEnv;
$mappings['prod']['ssh_target'] = $prodEnv->sshUrl;
}
break;
}
Expand Down
13 changes: 4 additions & 9 deletions src/Helpers/SshHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace Acquia\Cli\Helpers;

use Acquia\Cli\Exception\AcquiaCliException;
use AcquiaCloudApi\Response\EnvironmentResponse;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
Expand All @@ -32,20 +31,16 @@ public function __construct(
*
* @param int|null $timeout
*/
public function executeCommand(EnvironmentResponse|string $target, array $commandArgs, bool $printOutput = TRUE, int $timeout = NULL): Process {
public function executeCommand(string $sshUrl, array $commandArgs, bool $printOutput = TRUE, int $timeout = NULL): Process {
$commandSummary = $this->getCommandSummary($commandArgs);

if (is_a($target, EnvironmentResponse::class)) {
$target = $target->sshUrl;
}

// Remove site_env arg.
unset($commandArgs['alias']);
$process = $this->sendCommand($target, $commandArgs, $printOutput, $timeout);
$process = $this->sendCommand($sshUrl, $commandArgs, $printOutput, $timeout);

$this->logger->debug('Command: {command} [Exit: {exit}]', [
'command' => $commandSummary,
'env' => $target,
'env' => $sshUrl,
'exit' => $process->getExitCode(),
]);

Expand All @@ -56,7 +51,7 @@ public function executeCommand(EnvironmentResponse|string $target, array $comman
return $process;
}

private function sendCommand(?string $url, array $command, bool $printOutput, ?int $timeout = NULL): Process {
private function sendCommand(string $url, array $command, bool $printOutput, ?int $timeout = NULL): Process {
$command = array_values($this->getSshCommand($url, $command));
$this->localMachineHelper->checkRequiredBinariesExist(['ssh']);

Expand Down
35 changes: 18 additions & 17 deletions tests/phpunit/src/CommandTestBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use Acquia\Cli\Helpers\LocalMachineHelper;
use Acquia\Cli\Helpers\SshHelper;
use AcquiaCloudApi\Response\DatabaseResponse;
use AcquiaCloudApi\Response\EnvironmentResponse;
use Exception;
use Gitlab\Api\Projects;
use Gitlab\Api\Users;
Expand Down Expand Up @@ -82,7 +81,7 @@ protected function setCommand(CommandBase $command): void {
* An array of strings representing each input passed to the command input
* stream.
*/
protected function executeCommand(array $args = [], array $inputs = []): void {
protected function executeCommand(array $args = [], array $inputs = [], int $verbosity = Output::VERBOSITY_VERY_VERBOSE): void {
$cwd = $this->projectDir;
$tester = $this->getCommandTester();
$tester->setInputs($inputs);
Expand All @@ -96,7 +95,7 @@ protected function executeCommand(array $args = [], array $inputs = []): void {
}

try {
$tester->execute($args, ['verbosity' => Output::VERBOSITY_VERY_VERBOSE]);
$tester->execute($args, ['verbosity' => $verbosity]);
}
catch (Exception $e) {
if (getenv('ACLI_PRINT_COMMAND_OUTPUT')) {
Expand Down Expand Up @@ -264,7 +263,7 @@ protected function mockGetAcsfSites(mixed $sshHelper): array {
$multisiteConfig = file_get_contents(Path::join($this->realFixtureDir, '/multisite-config.json'));
$acsfMultisiteFetchProcess->getOutput()->willReturn($multisiteConfig)->shouldBeCalled();
$sshHelper->executeCommand(
Argument::type('object'),
Argument::type('string'),
['cat', '/var/www/site-php/profserv2.01dev/multisite-config.json'],
FALSE
)->willReturn($acsfMultisiteFetchProcess->reveal())->shouldBeCalled();
Expand All @@ -277,7 +276,7 @@ protected function mockGetCloudSites(mixed $sshHelper, mixed $environment): void
$parts = explode('.', $environment->ssh_url);
$sitegroup = reset($parts);
$sshHelper->executeCommand(
Argument::type('object'),
Argument::type('string'),
['ls', "/mnt/files/$sitegroup.{$environment->name}/sites"],
FALSE
)->willReturn($cloudMultisiteFetchProcess->reveal())->shouldBeCalled();
Expand Down Expand Up @@ -375,12 +374,12 @@ protected function mockNotificationResponseFromObject(object $responseWithNotifi
return $this->mockRequest('getNotificationByUuid', $uuid);
}

protected function mockCreateMySqlDumpOnLocal(ObjectProphecy $localMachineHelper): void {
protected function mockCreateMySqlDumpOnLocal(ObjectProphecy $localMachineHelper, bool $printOutput = TRUE): void {
$localMachineHelper->checkRequiredBinariesExist(["mysqldump", "gzip"])->shouldBeCalled();
$process = $this->mockProcess();
$process->getOutput()->willReturn('');
$command = 'MYSQL_PWD=drupal mysqldump --host=localhost --user=drupal drupal | pv --rate --bytes | gzip -9 > ' . sys_get_temp_dir() . '/acli-mysql-dump-drupal.sql.gz';
$localMachineHelper->executeFromCmd($command, Argument::type('callable'), NULL, TRUE)->willReturn($process->reveal())
$localMachineHelper->executeFromCmd($command, Argument::type('callable'), NULL, $printOutput)->willReturn($process->reveal())
->shouldBeCalled();
}

Expand Down Expand Up @@ -420,7 +419,7 @@ protected function setUpdateClient(int $statusCode = 200): void {
$this->command->setUpdateClient($guzzleClient->reveal());
}

protected function mockPollCloudViaSsh(object $environmentsResponse): ObjectProphecy {
protected function mockPollCloudViaSsh(array $environmentsResponse, bool $ssh = TRUE): ObjectProphecy {
$process = $this->prophet->prophesize(Process::class);
$process->isSuccessful()->willReturn(TRUE);
$process->getExitCode()->willReturn(0);
Expand All @@ -429,18 +428,20 @@ protected function mockPollCloudViaSsh(object $environmentsResponse): ObjectProp
$gitProcess->getExitCode()->willReturn(128);
$sshHelper = $this->mockSshHelper();
// Mock Git.
$urlParts = explode(':', $environmentsResponse->_embedded->items[0]->vcs->url);
$urlParts = explode(':', $environmentsResponse[0]->vcs->url);
$sshHelper->executeCommand($urlParts[0], ['ls'], FALSE)
->willReturn($gitProcess->reveal())
->shouldBeCalled();
// Mock non-prod.
$sshHelper->executeCommand(new EnvironmentResponse($environmentsResponse->_embedded->items[0]), ['ls'], FALSE)
->willReturn($process->reveal())
->shouldBeCalled();
// Mock prod.
$sshHelper->executeCommand(new EnvironmentResponse($environmentsResponse->_embedded->items[1]), ['ls'], FALSE)
->willReturn($process->reveal())
->shouldBeCalled();
if ($ssh) {
// Mock non-prod.
$sshHelper->executeCommand($environmentsResponse[0]->ssh_url, ['ls'], FALSE)
->willReturn($process->reveal())
->shouldBeCalled();
// Mock prod.
$sshHelper->executeCommand($environmentsResponse[1]->ssh_url, ['ls'], FALSE)
->willReturn($process->reveal())
->shouldBeCalled();
}
return $sshHelper;
}

Expand Down
34 changes: 30 additions & 4 deletions tests/phpunit/src/Commands/App/LogTailCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Acquia\Cli\Command\App\LogTailCommand;
use Acquia\Cli\Command\CommandBase;
use Acquia\Cli\Exception\AcquiaCliException;
use Acquia\Cli\Tests\CommandTestBase;
use AcquiaLogstream\LogstreamManager;
use Prophecy\Argument;
Expand All @@ -32,10 +33,6 @@ protected function createCommand(): CommandBase {
// Must initialize this here instead of in setUp() because we need the
// prophet to be initialized first.
$this->logStreamManagerProphecy = $this->prophet->prophesize(LogstreamManager::class);
$this->logStreamManagerProphecy->setColourise(TRUE)->shouldBeCalled();
$this->logStreamManagerProphecy->setParams(Argument::type('object'))->shouldBeCalled();
$this->logStreamManagerProphecy->setLogTypeFilter(["bal-request"])->shouldBeCalled();
$this->logStreamManagerProphecy->stream()->shouldBeCalled();

return new LogTailCommand(
$this->localMachineHelper,
Expand All @@ -56,6 +53,10 @@ protected function createCommand(): CommandBase {
* @dataProvider providerLogTailCommand
*/
public function testLogTailCommand(?int $stream): void {
$this->logStreamManagerProphecy->setColourise(TRUE)->shouldBeCalled();
$this->logStreamManagerProphecy->setParams(Argument::type('object'))->shouldBeCalled();
$this->logStreamManagerProphecy->setLogTypeFilter(["bal-request"])->shouldBeCalled();
$this->logStreamManagerProphecy->stream()->shouldBeCalled();
$this->mockGetEnvironment();
$this->mockLogStreamRequest();
$this->executeCommand([], [
Expand Down Expand Up @@ -95,6 +96,31 @@ public function testLogTailCommandWithEnvArg(): void {
$this->assertStringContainsString('Drupal request', $output);
}

public function testLogTailNode(): void {
$applications = $this->mockRequest('getApplications');
$application = $this->mockRequest('getApplicationByUuid', $applications[self::$INPUT_DEFAULT_CHOICE]->uuid);
$tamper = function ($responses): void {
foreach ($responses as $response) {
$response->type = 'node';
}
};
$this->mockRequest('getApplicationEnvironments', $application->uuid, NULL, NULL, $tamper);
$this->expectException(AcquiaCliException::class);
$this->expectExceptionMessage('No compatible environments found');
$this->executeCommand([], [
// Would you like Acquia CLI to search for a Cloud application that matches your local git config?
'n',
// Select the application.
0,
// Would you like to link the project at ... ?
'y',
// Select environment.
0,
// Select log.
0,
]);
}

private function mockLogStreamRequest(): void {
$response = $this->getMockResponseFromSpec('/environments/{environmentId}/logstream',
'get', '200');
Expand Down
Loading

0 comments on commit 012ac02

Please sign in to comment.