Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI-1330: TypeError for SSH commands on Node environments #1734

Merged
merged 13 commits into from
May 8, 2024
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 @@
*
* @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 {

Check warning on line 34 in src/Helpers/SshHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ * * @param int|null $timeout */ - public function executeCommand(string $sshUrl, array $commandArgs, bool $printOutput = TRUE, int $timeout = NULL) : Process + public function executeCommand(string $sshUrl, array $commandArgs, bool $printOutput = false, int $timeout = NULL) : Process { $commandSummary = $this->getCommandSummary($commandArgs); // Remove site_env arg.
$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}]', [

Check warning on line 41 in src/Helpers/SshHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing

Escaped Mutant for Mutator "ArrayItemRemoval": --- Original +++ New @@ @@ // Remove site_env arg. unset($commandArgs['alias']); $process = $this->sendCommand($sshUrl, $commandArgs, $printOutput, $timeout); - $this->logger->debug('Command: {command} [Exit: {exit}]', ['command' => $commandSummary, 'env' => $sshUrl, 'exit' => $process->getExitCode()]); + $this->logger->debug('Command: {command} [Exit: {exit}]', ['env' => $sshUrl, 'exit' => $process->getExitCode()]); if (!$process->isSuccessful() && $process->getExitCode() === 255) { throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput()); }

Check warning on line 41 in src/Helpers/SshHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ // Remove site_env arg. unset($commandArgs['alias']); $process = $this->sendCommand($sshUrl, $commandArgs, $printOutput, $timeout); - $this->logger->debug('Command: {command} [Exit: {exit}]', ['command' => $commandSummary, 'env' => $sshUrl, 'exit' => $process->getExitCode()]); + if (!$process->isSuccessful() && $process->getExitCode() === 255) { throw new AcquiaCliException($process->getOutput() . $process->getErrorOutput()); }
'command' => $commandSummary,
'env' => $target,
'env' => $sshUrl,
'exit' => $process->getExitCode(),
]);

Expand All @@ -56,7 +51,7 @@
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
Loading