From 10ddc0c8f621765ec2d7696dffef6bd5af396e46 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Wed, 17 Nov 2021 17:18:55 +0000 Subject: [PATCH 01/23] Refactor BaseCommand to handle commands without output --- app/Commands/BaseCommand.php | 100 ++-------------------- app/Commands/Concerns/InteractsWithIO.php | 89 +++++++++++++++++++ app/Commands/ConfigureCommand.php | 4 +- app/Commands/Servers/ListCommand.php | 25 +++--- app/Commands/Sites/ListCommand.php | 29 ++++--- 5 files changed, 132 insertions(+), 115 deletions(-) create mode 100644 app/Commands/Concerns/InteractsWithIO.php diff --git a/app/Commands/BaseCommand.php b/app/Commands/BaseCommand.php index 1109363..f978e36 100644 --- a/app/Commands/BaseCommand.php +++ b/app/Commands/BaseCommand.php @@ -6,9 +6,7 @@ use DeliciousBrains\SpinupWp\SpinupWp; use Exception; use GuzzleHttp\Client; -use Illuminate\Support\Collection; use LaravelZero\Framework\Commands\Command; -use Symfony\Component\Console\Formatter\OutputFormatterStyle; abstract class BaseCommand extends Command { @@ -30,14 +28,15 @@ public function handle(): int { if ($this->requiresToken && !$this->config->isConfigured()) { $this->error("You must first run 'spinupwp configure' in order to set up your API token."); - return 1; + return self::FAILURE; } try { if (!$this->spinupwp->hasApiKey()) { $this->spinupwp->setApiKey($this->apiToken())->setClient(); } - // allow to use a different API URL + + // Allow to use a different API URL if (!empty($this->config->get('api_url', $this->profile()))) { $this->spinupwp->setClient( new Client([ @@ -52,21 +51,21 @@ public function handle(): int ); } - $this->format($this->action()); - - return 0; + return $this->action(); } catch (Exception $e) { $this->error($e->getMessage()); - return 1; + return self::FAILURE; } } protected function apiToken(): string { $apiToken = $this->config->get('api_token', $this->profile()); + if (!$apiToken) { throw new Exception("The API token for the profile {$this->profile()} is not yet configured"); } + return $apiToken; } @@ -75,90 +74,9 @@ protected function profile(): string if (is_string($this->option('profile'))) { return $this->option('profile'); } - return 'default'; - } - - protected function format($resource): void - { - $this->setStyles(); - - if ($this->displayFormat() === 'table') { - $this->toTable($resource); - return; - } - $this->toJson($resource); - } - - protected function setStyles(): void - { - if (!$this->output->getFormatter()->hasStyle('enabled')) { - $this->output->getFormatter()->setStyle( - 'enabled', - new OutputFormatterStyle('green'), - ); - } - - if (!$this->output->getFormatter()->hasStyle('disabled')) { - $this->output->getFormatter()->setStyle( - 'disabled', - new OutputFormatterStyle('red'), - ); - } - } - - protected function displayFormat(): string - { - if (is_string($this->option('format'))) { - return $this->option('format'); - } - return $this->config->get('format', $this->profile()); - } - - protected function toJson($resource): void - { - $this->line(json_encode($resource->toArray(), JSON_PRETTY_PRINT)); - } - - protected function toTable($resource): void - { - $tableHeaders = []; - - if ($resource instanceof Collection) { - $firstElement = $resource->first(); - - if (!is_array($firstElement)) { - $firstElement = $firstElement->toArray(); - } - - $tableHeaders = array_keys($firstElement); - - $rows = []; - - $resource->each(function ($item) use (&$rows) { - if (!is_array($item)) { - $item->toArray(); - } - - $row = array_map(function ($value) { - if (is_array($value)) { - $value = ''; - } - if (is_bool($value)) { - $value = $value ? 'Y' : 'N'; - } - return $value; - }, array_values($item)); - - $rows[] = $row; - }); - } - - $this->table( - $tableHeaders, - $rows, - ); + return 'default'; } - abstract protected function action(); + abstract protected function action(): int; } diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php new file mode 100644 index 0000000..2809420 --- /dev/null +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -0,0 +1,89 @@ +setStyles(); + + if ($this->displayFormat() === 'table') { + $this->toTable($resource); + return; + } + + $this->toJson($resource); + } + + protected function setStyles(): void + { + if (!$this->output->getFormatter()->hasStyle('enabled')) { + $this->output->getFormatter()->setStyle( + 'enabled', + new OutputFormatterStyle('green'), + ); + } + + if (!$this->output->getFormatter()->hasStyle('disabled')) { + $this->output->getFormatter()->setStyle( + 'disabled', + new OutputFormatterStyle('red'), + ); + } + } + + protected function displayFormat(): string + { + if (is_string($this->option('format'))) { + return $this->option('format'); + } + + return $this->config->get('format', $this->profile()); + } + + protected function toJson($resource): void + { + $this->line(json_encode($resource->toArray(), JSON_PRETTY_PRINT)); + } + + protected function toTable($resource): void + { + $tableHeaders = []; + + if ($resource instanceof Collection) { + $firstElement = $resource->first(); + + if (!is_array($firstElement)) { + $firstElement = $firstElement->toArray(); + } + + $tableHeaders = array_keys($firstElement); + + $rows = []; + + $resource->each(function ($item) use (&$rows) { + if (!is_array($item)) { + $item->toArray(); + } + + $row = array_map(function ($value) { + if (is_array($value)) { + $value = ''; + } + if (is_bool($value)) { + $value = $value ? 'Y' : 'N'; + } + return $value; + }, array_values($item)); + + $rows[] = $row; + }); + } + + $this->table($tableHeaders, $rows); + } +} diff --git a/app/Commands/ConfigureCommand.php b/app/Commands/ConfigureCommand.php index 13e9ac1..399d757 100644 --- a/app/Commands/ConfigureCommand.php +++ b/app/Commands/ConfigureCommand.php @@ -51,8 +51,8 @@ public function handle(): int return 0; } - protected function action() + protected function action(): int { - return null; + return self::INVALID; } } diff --git a/app/Commands/Servers/ListCommand.php b/app/Commands/Servers/ListCommand.php index 349d7dd..7d59434 100644 --- a/app/Commands/Servers/ListCommand.php +++ b/app/Commands/Servers/ListCommand.php @@ -3,27 +3,32 @@ namespace App\Commands\Servers; use App\Commands\BaseCommand; +use App\Commands\Concerns\InteractsWithIO; class ListCommand extends BaseCommand { + use InteractsWithIO; + protected $signature = 'servers:list {--format=} {--profile=}'; protected $description = 'Retrieves a list of servers'; - protected function action() + protected function action(): int { $servers = collect($this->spinupwp->servers->list()); - if ($this->displayFormat() === 'json') { - return $servers; + if ($this->displayFormat() === 'table') { + $servers->transform(fn($server) => [ + 'ID' => $server->id, + 'Name' => $server->name, + 'IP Address' => $server->ip_address, + 'Ubuntu' => $server->ubuntu_version, + 'Database' => $server->database['server'], + ]); } - return $servers->map(fn ($server) => [ - 'ID' => $server->id, - 'Name' => $server->name, - 'IP Address' => $server->ip_address, - 'Ubuntu' => $server->ubuntu_version, - 'Database' => $server->database['server'], - ]); + $this->format($servers); + + return self::SUCCESS; } } diff --git a/app/Commands/Sites/ListCommand.php b/app/Commands/Sites/ListCommand.php index 99552a9..2e8fdb5 100644 --- a/app/Commands/Sites/ListCommand.php +++ b/app/Commands/Sites/ListCommand.php @@ -3,14 +3,17 @@ namespace App\Commands\Sites; use App\Commands\BaseCommand; +use App\Commands\Concerns\InteractsWithIO; class ListCommand extends BaseCommand { + use InteractsWithIO; + protected $signature = 'sites:list {server_id? : Only list sites belonging to this server} {--format=} {--profile=}'; protected $description = 'Retrieves a list of sites'; - protected function action() + protected function action(): int { $serverId = $this->argument('server_id'); @@ -20,18 +23,20 @@ protected function action() $sites = collect($this->spinupwp->sites->list()); } - if ($this->displayFormat() === 'json') { - return $sites; + if ($this->displayFormat() === 'table') { + $sites->transform(fn($site) => [ + 'ID' => $site->id, + 'Server ID' => $site->server_id, + 'Domain' => $site->domain, + 'Site User' => $site->site_user, + 'PHP' => $site->php_version, + 'Page Cache' => $site->page_cache['enabled'], + 'HTTPS' => $site->https['enabled'], + ]); } - return $sites->map(fn ($site) => [ - 'ID' => $site->id, - 'Server ID' => $site->server_id, - 'Domain' => $site->domain, - 'Site User' => $site->site_user, - 'PHP' => $site->php_version, - 'Page Cache' => $site->page_cache['enabled'], - 'HTTPS' => $site->https['enabled'], - ]); + $this->format($sites); + + return self::SUCCESS; } } From da6135c6aa6c44effd2b76ccb02d65349b846672 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Wed, 17 Nov 2021 17:34:04 +0000 Subject: [PATCH 02/23] Add SSH command and allow site selection --- app/Commands/Concerns/InteractsWithIO.php | 23 +++++++++++++++++++ app/Commands/Sites/SshCommand.php | 28 +++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 app/Commands/Sites/SshCommand.php diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php index 2809420..53a301c 100644 --- a/app/Commands/Concerns/InteractsWithIO.php +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Question\ChoiceQuestion; trait InteractsWithIO { @@ -86,4 +87,26 @@ protected function toTable($resource): void $this->table($tableHeaders, $rows); } + + public function askToSelectSite(string $question): int + { + $choices = collect($this->spinupwp->sites->list()); + + return $this->askToSelect( + $question, + $choices->keyBy('id')->map(fn($site) => $site->domain)->toArray() + ); + } + + protected function askToSelect(string $question, array $choices, $default = null): int + { + $question = new class($question, $choices, $default) extends ChoiceQuestion { + public function isAssoc(array $array): bool + { + return true; + } + }; + + return (int)$this->output->askQuestion($question); + } } diff --git a/app/Commands/Sites/SshCommand.php b/app/Commands/Sites/SshCommand.php new file mode 100644 index 0000000..8c86f5c --- /dev/null +++ b/app/Commands/Sites/SshCommand.php @@ -0,0 +1,28 @@ +argument('site_id'); + + if (empty($siteId)) { + $siteId = $this->askToSelectSite('Which site would you like to start an SSH session for'); + } + + $site = $this->spinupwp->sites->get((int)$siteId); + + return self::SUCCESS; + } +} From d62fbfd42525bbc10365a0897cfab3213a1defc9 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Wed, 17 Nov 2021 18:00:15 +0000 Subject: [PATCH 03/23] Start an SSH session with site --- app/Commands/Concerns/InteractsWithRemote.php | 13 +++++++++++++ app/Commands/Sites/SshCommand.php | 13 +++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 app/Commands/Concerns/InteractsWithRemote.php diff --git a/app/Commands/Concerns/InteractsWithRemote.php b/app/Commands/Concerns/InteractsWithRemote.php new file mode 100644 index 0000000..db6f295 --- /dev/null +++ b/app/Commands/Concerns/InteractsWithRemote.php @@ -0,0 +1,13 @@ +askToSelectSite('Which site would you like to start an SSH session for'); } - $site = $this->spinupwp->sites->get((int)$siteId); + $site = $this->spinupwp->sites->get((int)$siteId); + $server = $this->spinupwp->servers->get($site->server_id); - return self::SUCCESS; + $this->line("Establishing a secure connection to [{$server->name}] as [{$site->site_user}]..."); + + return $this->ssh( + $site->site_user, + $server->ip_address, + $server->ssh_port, + ); } } From e089f25d78c1ba272ffb89479303854a48b5c598 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Wed, 17 Nov 2021 18:04:22 +0000 Subject: [PATCH 04/23] Set SSH options --- app/Commands/Concerns/InteractsWithRemote.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Commands/Concerns/InteractsWithRemote.php b/app/Commands/Concerns/InteractsWithRemote.php index db6f295..926674d 100644 --- a/app/Commands/Concerns/InteractsWithRemote.php +++ b/app/Commands/Concerns/InteractsWithRemote.php @@ -6,7 +6,7 @@ trait InteractsWithRemote { protected function ssh($user, $host, int $port = 22): int { - passthru("ssh -t {$user}@{$host} -p {$port}", $exitCode); + passthru("ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -t {$user}@{$host} -p {$port}", $exitCode); return (int) $exitCode; } From b6723b64b379ebeb25fd7b01e9ff71f909b37323 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 09:46:37 +0000 Subject: [PATCH 05/23] Reuse SSH connections --- app/Commands/Concerns/InteractsWithRemote.php | 14 ++++++++++++-- app/Helpers/Configuration.php | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/Commands/Concerns/InteractsWithRemote.php b/app/Commands/Concerns/InteractsWithRemote.php index 926674d..56ab4ad 100644 --- a/app/Commands/Concerns/InteractsWithRemote.php +++ b/app/Commands/Concerns/InteractsWithRemote.php @@ -6,8 +6,18 @@ trait InteractsWithRemote { protected function ssh($user, $host, int $port = 22): int { - passthru("ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -t {$user}@{$host} -p {$port}", $exitCode); + $options = collect([ + 'ConnectTimeout' => 5, + 'ControlMaster' => 'auto', + 'ControlPath' => $this->config->sshControlPath(), + 'ControlPersist' => 100, + 'LogLevel' => 'QUIET', + ])->map(function ($value, $option) { + return "-o $option=$value"; + })->implode(' '); - return (int) $exitCode; + passthru("ssh {$options} -t {$user}@{$host} -p {$port}", $exitCode); + + return (int)$exitCode; } } diff --git a/app/Helpers/Configuration.php b/app/Helpers/Configuration.php index 5e60641..b76604b 100644 --- a/app/Helpers/Configuration.php +++ b/app/Helpers/Configuration.php @@ -3,6 +3,7 @@ namespace App\Helpers; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\File; class Configuration { @@ -74,10 +75,21 @@ protected function readConfig(): array public function configFilePath(): string { - if (!file_exists($this->path)) { - mkdir($this->path); + if (!File::isDirectory($this->path)) { + File::makeDirectory($this->path); } return $this->path . 'config.json'; } + + public function sshControlPath(): string + { + $sshPath = $this->path . 'ssh/'; + + if (!File::isDirectory($sshPath)) { + File::makeDirectory($sshPath); + } + + return $sshPath . '%h-%p-%r'; + } } From ba3d959a16036bb27efa8ad451599a6fbb142bb6 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 10:44:00 +0000 Subject: [PATCH 06/23] Add -f and -l flags --- app/Commands/Concerns/InteractsWithRemote.php | 4 ++-- app/Commands/Sites/SshCommand.php | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/Commands/Concerns/InteractsWithRemote.php b/app/Commands/Concerns/InteractsWithRemote.php index 56ab4ad..2fe10bc 100644 --- a/app/Commands/Concerns/InteractsWithRemote.php +++ b/app/Commands/Concerns/InteractsWithRemote.php @@ -4,7 +4,7 @@ trait InteractsWithRemote { - protected function ssh($user, $host, int $port = 22): int + protected function ssh(string $user, string $host, int $port = 22, string $command = ''): int { $options = collect([ 'ConnectTimeout' => 5, @@ -16,7 +16,7 @@ protected function ssh($user, $host, int $port = 22): int return "-o $option=$value"; })->implode(' '); - passthru("ssh {$options} -t {$user}@{$host} -p {$port}", $exitCode); + passthru("ssh {$options} -t {$user}@{$host} -p {$port} '{$command}'", $exitCode); return (int)$exitCode; } diff --git a/app/Commands/Sites/SshCommand.php b/app/Commands/Sites/SshCommand.php index fc46dbe..6ea31d3 100644 --- a/app/Commands/Sites/SshCommand.php +++ b/app/Commands/Sites/SshCommand.php @@ -11,7 +11,11 @@ class SshCommand extends BaseCommand use InteractsWithIO; use InteractsWithRemote; - protected $signature = 'sites:ssh {site_id?} {--profile=}'; + protected $signature = 'sites:ssh + {site_id?} + {--f|files : Navigate to the files directory} + {--l|logs : Navigate to the logs directory} + {--profile=}'; protected $description = 'Start an SSH session as the site user'; @@ -32,6 +36,22 @@ protected function action(): int $site->site_user, $server->ip_address, $server->ssh_port, + $this->cdCommand(), ); } + + protected function cdCommand(): string + { + $cdCommand = ''; + $cdFlags = ['files', 'logs']; + + foreach ($cdFlags as $flag) { + if ($this->option($flag)) { + $cdCommand = "cd ./{$flag}; bash --login"; + break; + } + } + + return $cdCommand; + } } From d6fdb6f72d0c08588b03e12b00be623050f0c2ea Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 10:56:45 +0000 Subject: [PATCH 07/23] Add SSH servers comment --- app/Commands/Concerns/InteractsWithIO.php | 10 +++++ app/Commands/Servers/SshCommand.php | 45 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 app/Commands/Servers/SshCommand.php diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php index 53a301c..bf85cf4 100644 --- a/app/Commands/Concerns/InteractsWithIO.php +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -98,6 +98,16 @@ public function askToSelectSite(string $question): int ); } + public function askToSelectServer(string $question): int + { + $choices = collect($this->spinupwp->servers->list()); + + return $this->askToSelect( + $question, + $choices->keyBy('id')->map(fn($server) => $server->name)->toArray() + ); + } + protected function askToSelect(string $question, array $choices, $default = null): int { $question = new class($question, $choices, $default) extends ChoiceQuestion { diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php new file mode 100644 index 0000000..0aa5167 --- /dev/null +++ b/app/Commands/Servers/SshCommand.php @@ -0,0 +1,45 @@ +argument('server_id'); + + if (empty($serverId)) { + $serverId = $this->askToSelectServer('Which server would you like to start an SSH session for'); + } + + $user = $this->argument('user'); + + if (empty($user)) { + $user = $this->ask('Which user would you like to connect as'); + } + + $server = $this->spinupwp->servers->get((int)$serverId); + + $this->line("Establishing a secure connection to [{$server->name}] as [{$user}]..."); + + return $this->ssh( + $user, + $server->ip_address, + $server->ssh_port, + ); + } +} From 97e35bba84bbd0c10f303f83cef6e09d77972ec9 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 12:16:39 +0000 Subject: [PATCH 08/23] Make it possible to save default SSH user --- app/Commands/Servers/SshCommand.php | 43 +++++++++++++++++++++++++---- app/Helpers/Configuration.php | 8 +++--- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php index 0aa5167..ab1728d 100644 --- a/app/Commands/Servers/SshCommand.php +++ b/app/Commands/Servers/SshCommand.php @@ -26,13 +26,8 @@ protected function action(): int $serverId = $this->askToSelectServer('Which server would you like to start an SSH session for'); } - $user = $this->argument('user'); - - if (empty($user)) { - $user = $this->ask('Which user would you like to connect as'); - } - $server = $this->spinupwp->servers->get((int)$serverId); + $user = $this->establishUser(); $this->line("Establishing a secure connection to [{$server->name}] as [{$user}]..."); @@ -42,4 +37,40 @@ protected function action(): int $server->ssh_port, ); } + + protected function establishUser(): string + { + $user = $this->argument('user'); + $defaultUser = $this->config->get('default_ssh_user', $this->profile(), null); + + if (is_string($user)) { + if (is_null($defaultUser)) { + $this->askToSetDefaultUser($user); + } + + return $user; + } + + if (!empty($defaultUser)) { + return $defaultUser; + } + + $user = $this->ask('Which user would you like to connect as'); + + if (is_null($defaultUser)) { + $this->askToSetDefaultUser($user); + } + + return $user; + } + + protected function askToSetDefaultUser(string $user): void + { + do { + $response = strtolower($this->ask("Do you want to set \"{$user}\" as your default SSH user? (y/n)", 'y')); + } while (!in_array($response, ['y', 'n'])); + + $value = $response === 'y' ? $user : ''; + $this->config->set('default_ssh_user', $value, $this->profile()); + } } diff --git a/app/Helpers/Configuration.php b/app/Helpers/Configuration.php index b76604b..0d034d1 100644 --- a/app/Helpers/Configuration.php +++ b/app/Helpers/Configuration.php @@ -22,21 +22,21 @@ public function isConfigured(): bool return file_exists($this->configFilePath()); } - public function get(string $key, string $profile = 'default'): string + public function get(string $key, string $profile = 'default', ?string $default = ''): ?string { $this->config = $this->readConfig(); if (empty($this->config)) { - return ''; + return $default; } if (!$this->teamExists($profile)) { - return ''; + return $default; } $profileConfig = $this->config[$profile]; if (!isset($profileConfig[$key])) { - return ''; + return $default; } return $profileConfig[$key]; From 08f41392c5b80f0bf3ab7c6961d529938c4dce1f Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 12:19:53 +0000 Subject: [PATCH 09/23] Improve list command descriptions --- app/Commands/Servers/ListCommand.php | 6 ++++-- app/Commands/Sites/ListCommand.php | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/Commands/Servers/ListCommand.php b/app/Commands/Servers/ListCommand.php index 7d59434..5c6b58a 100644 --- a/app/Commands/Servers/ListCommand.php +++ b/app/Commands/Servers/ListCommand.php @@ -9,9 +9,11 @@ class ListCommand extends BaseCommand { use InteractsWithIO; - protected $signature = 'servers:list {--format=} {--profile=}'; + protected $signature = 'servers:list + {--format=} + {--profile=}'; - protected $description = 'Retrieves a list of servers'; + protected $description = 'List all servers'; protected function action(): int { diff --git a/app/Commands/Sites/ListCommand.php b/app/Commands/Sites/ListCommand.php index 2e8fdb5..987d94b 100644 --- a/app/Commands/Sites/ListCommand.php +++ b/app/Commands/Sites/ListCommand.php @@ -9,9 +9,12 @@ class ListCommand extends BaseCommand { use InteractsWithIO; - protected $signature = 'sites:list {server_id? : Only list sites belonging to this server} {--format=} {--profile=}'; + protected $signature = 'sites:list + {server_id? : Only list sites belonging to this server} + {--format=} + {--profile=}'; - protected $description = 'Retrieves a list of sites'; + protected $description = 'List all sites'; protected function action(): int { From 86497f205563ff2071688f6b85ddc2371d8070f2 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 12:33:55 +0000 Subject: [PATCH 10/23] Improve argument descriptions --- app/Commands/Servers/SshCommand.php | 4 ++-- app/Commands/Sites/SshCommand.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php index ab1728d..89dec4c 100644 --- a/app/Commands/Servers/SshCommand.php +++ b/app/Commands/Servers/SshCommand.php @@ -12,8 +12,8 @@ class SshCommand extends BaseCommand use InteractsWithRemote; protected $signature = 'servers:ssh - {server_id?} - {user?} + {server_id? : The server to connect to} + {user? : The SSH user to connect as} {--profile=}'; protected $description = 'Start an SSH session'; diff --git a/app/Commands/Sites/SshCommand.php b/app/Commands/Sites/SshCommand.php index 6ea31d3..aa4d10a 100644 --- a/app/Commands/Sites/SshCommand.php +++ b/app/Commands/Sites/SshCommand.php @@ -12,7 +12,7 @@ class SshCommand extends BaseCommand use InteractsWithRemote; protected $signature = 'sites:ssh - {site_id?} + {site_id? : The site to connect to} {--f|files : Navigate to the files directory} {--l|logs : Navigate to the logs directory} {--profile=}'; From 5b21301d28779484b348abed28f126f02bbe8070 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 14:12:41 +0000 Subject: [PATCH 11/23] Better error handling when SSH connection fails --- app/Commands/Servers/SshCommand.php | 8 +++++++- app/Commands/Sites/SshCommand.php | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php index 89dec4c..355e525 100644 --- a/app/Commands/Servers/SshCommand.php +++ b/app/Commands/Servers/SshCommand.php @@ -31,11 +31,17 @@ protected function action(): int $this->line("Establishing a secure connection to [{$server->name}] as [{$user}]..."); - return $this->ssh( + $exitCode = $this->ssh( $user, $server->ip_address, $server->ssh_port, ); + + if ($exitCode === 255) { + $this->error("Unable to connect to \"{$server->name}\". Have you added your SSH key to the \"{$user}\" user?"); + } + + return $exitCode; } protected function establishUser(): string diff --git a/app/Commands/Sites/SshCommand.php b/app/Commands/Sites/SshCommand.php index aa4d10a..27d8f76 100644 --- a/app/Commands/Sites/SshCommand.php +++ b/app/Commands/Sites/SshCommand.php @@ -32,12 +32,18 @@ protected function action(): int $this->line("Establishing a secure connection to [{$server->name}] as [{$site->site_user}]..."); - return $this->ssh( + $exitCode = $this->ssh( $site->site_user, $server->ip_address, $server->ssh_port, $this->cdCommand(), ); + + if ($exitCode === 255) { + $this->error("Unable to connect to \"{$server->name}\". Have you added your SSH key to the \"{$site->site_user}\" user?"); + } + + return $exitCode; } protected function cdCommand(): string From b7a213b32648d9bffdb86289c625d454319d2b84 Mon Sep 17 00:00:00 2001 From: A5hleyRich Date: Thu, 18 Nov 2021 15:37:54 +0000 Subject: [PATCH 12/23] Apply php-cs-fixer changes --- app/Commands/Concerns/InteractsWithIO.php | 6 +++--- app/Commands/Concerns/InteractsWithRemote.php | 2 +- app/Commands/Servers/ListCommand.php | 2 +- app/Commands/Servers/SshCommand.php | 2 +- app/Commands/Sites/ListCommand.php | 2 +- app/Commands/Sites/SshCommand.php | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php index bf85cf4..6cdad0c 100644 --- a/app/Commands/Concerns/InteractsWithIO.php +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -94,7 +94,7 @@ public function askToSelectSite(string $question): int return $this->askToSelect( $question, - $choices->keyBy('id')->map(fn($site) => $site->domain)->toArray() + $choices->keyBy('id')->map(fn ($site) => $site->domain)->toArray() ); } @@ -104,7 +104,7 @@ public function askToSelectServer(string $question): int return $this->askToSelect( $question, - $choices->keyBy('id')->map(fn($server) => $server->name)->toArray() + $choices->keyBy('id')->map(fn ($server) => $server->name)->toArray() ); } @@ -117,6 +117,6 @@ public function isAssoc(array $array): bool } }; - return (int)$this->output->askQuestion($question); + return (int) $this->output->askQuestion($question); } } diff --git a/app/Commands/Concerns/InteractsWithRemote.php b/app/Commands/Concerns/InteractsWithRemote.php index 2fe10bc..703f1af 100644 --- a/app/Commands/Concerns/InteractsWithRemote.php +++ b/app/Commands/Concerns/InteractsWithRemote.php @@ -18,6 +18,6 @@ protected function ssh(string $user, string $host, int $port = 22, string $comma passthru("ssh {$options} -t {$user}@{$host} -p {$port} '{$command}'", $exitCode); - return (int)$exitCode; + return (int) $exitCode; } } diff --git a/app/Commands/Servers/ListCommand.php b/app/Commands/Servers/ListCommand.php index 5c6b58a..8f2944d 100644 --- a/app/Commands/Servers/ListCommand.php +++ b/app/Commands/Servers/ListCommand.php @@ -20,7 +20,7 @@ protected function action(): int $servers = collect($this->spinupwp->servers->list()); if ($this->displayFormat() === 'table') { - $servers->transform(fn($server) => [ + $servers->transform(fn ($server) => [ 'ID' => $server->id, 'Name' => $server->name, 'IP Address' => $server->ip_address, diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php index 355e525..c6a8ef5 100644 --- a/app/Commands/Servers/SshCommand.php +++ b/app/Commands/Servers/SshCommand.php @@ -26,7 +26,7 @@ protected function action(): int $serverId = $this->askToSelectServer('Which server would you like to start an SSH session for'); } - $server = $this->spinupwp->servers->get((int)$serverId); + $server = $this->spinupwp->servers->get((int) $serverId); $user = $this->establishUser(); $this->line("Establishing a secure connection to [{$server->name}] as [{$user}]..."); diff --git a/app/Commands/Sites/ListCommand.php b/app/Commands/Sites/ListCommand.php index 987d94b..534b048 100644 --- a/app/Commands/Sites/ListCommand.php +++ b/app/Commands/Sites/ListCommand.php @@ -27,7 +27,7 @@ protected function action(): int } if ($this->displayFormat() === 'table') { - $sites->transform(fn($site) => [ + $sites->transform(fn ($site) => [ 'ID' => $site->id, 'Server ID' => $site->server_id, 'Domain' => $site->domain, diff --git a/app/Commands/Sites/SshCommand.php b/app/Commands/Sites/SshCommand.php index 27d8f76..5adbf41 100644 --- a/app/Commands/Sites/SshCommand.php +++ b/app/Commands/Sites/SshCommand.php @@ -27,7 +27,7 @@ protected function action(): int $siteId = $this->askToSelectSite('Which site would you like to start an SSH session for'); } - $site = $this->spinupwp->sites->get((int)$siteId); + $site = $this->spinupwp->sites->get((int) $siteId); $server = $this->spinupwp->servers->get($site->server_id); $this->line("Establishing a secure connection to [{$server->name}] as [{$site->site_user}]..."); From 391552f51b4f0da3dbbc5085ec2e43a769026f50 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 16:15:50 +0000 Subject: [PATCH 13/23] Rename ssh_user config key --- app/Commands/Servers/SshCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php index c6a8ef5..c93ba73 100644 --- a/app/Commands/Servers/SshCommand.php +++ b/app/Commands/Servers/SshCommand.php @@ -47,7 +47,7 @@ protected function action(): int protected function establishUser(): string { $user = $this->argument('user'); - $defaultUser = $this->config->get('default_ssh_user', $this->profile(), null); + $defaultUser = $this->config->get('ssh_user', $this->profile(), null); if (is_string($user)) { if (is_null($defaultUser)) { @@ -77,6 +77,6 @@ protected function askToSetDefaultUser(string $user): void } while (!in_array($response, ['y', 'n'])); $value = $response === 'y' ? $user : ''; - $this->config->set('default_ssh_user', $value, $this->profile()); + $this->config->set('ssh_user', $value, $this->profile()); } } From 56a07aafd8565423bd0e3e7ddd2c02bca3e1bcd4 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Thu, 18 Nov 2021 17:51:20 +0000 Subject: [PATCH 14/23] Test sites:ssh --- app/Commands/Concerns/InteractsWithRemote.php | 2 +- config/app.php | 2 + .../Feature/Commands/SitesSshCommandTest.php | 81 +++++++++++++++++++ tests/Pest.php | 1 + 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Commands/SitesSshCommandTest.php diff --git a/app/Commands/Concerns/InteractsWithRemote.php b/app/Commands/Concerns/InteractsWithRemote.php index 703f1af..b6ba71f 100644 --- a/app/Commands/Concerns/InteractsWithRemote.php +++ b/app/Commands/Concerns/InteractsWithRemote.php @@ -7,7 +7,7 @@ trait InteractsWithRemote protected function ssh(string $user, string $host, int $port = 22, string $command = ''): int { $options = collect([ - 'ConnectTimeout' => 5, + 'ConnectTimeout' => config('app.ssh_timeout'), 'ControlMaster' => 'auto', 'ControlPath' => $this->config->sshControlPath(), 'ControlPersist' => 100, diff --git a/config/app.php b/config/app.php index dee755c..f3e783b 100644 --- a/config/app.php +++ b/config/app.php @@ -62,4 +62,6 @@ 'table', ], + 'ssh_timeout' => 5, + ]; diff --git a/tests/Feature/Commands/SitesSshCommandTest.php b/tests/Feature/Commands/SitesSshCommandTest.php new file mode 100644 index 0000000..1c88a1f --- /dev/null +++ b/tests/Feature/Commands/SitesSshCommandTest.php @@ -0,0 +1,81 @@ +clientMock->shouldReceive('request')->once()->with('GET', 'sites/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'server_id' => 1, + 'domain' => 'hellfishmedia.com', + 'site_user' => 'hellfish', + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('sites:ssh 1 --profile=johndoe') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [hellfish]...') + ->assertExitCode(255); +}); + +test('ssh command with no site ID', function () { + $this->clientMock->shouldReceive('request')->once()->with('GET', 'sites?page=1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + [ + 'id' => 1, + 'server_id' => 1, + 'domain' => 'hellfishmedia.com', + 'site_user' => 'hellfish', + ], + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'sites/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'server_id' => 1, + 'domain' => 'hellfishmedia.com', + 'site_user' => 'hellfish', + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('sites:ssh --profile=johndoe') + ->expectsQuestion('Which site would you like to start an SSH session for', '1') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [hellfish]...') + ->assertExitCode(255); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 92517b1..47e79d7 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -22,6 +22,7 @@ ->beforeEach(function () { $this->clientMock = Mockery::mock(Client::class); $this->spinupwp = resolve(SpinupWp::class)->setClient($this->clientMock)->setApiKey('123'); + config()->set('app.ssh_timeout', -1); }) ->in('Feature', 'Unit'); From c44bf1752544c61b630810e68ab581664e9d4e2c Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Mon, 22 Nov 2021 17:02:56 +0000 Subject: [PATCH 15/23] Test servers:ssh --- .../Commands/ServersSshCommandTest.php | 103 ++++++++++++++++++ tests/Pest.php | 8 +- 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/Commands/ServersSshCommandTest.php diff --git a/tests/Feature/Commands/ServersSshCommandTest.php b/tests/Feature/Commands/ServersSshCommandTest.php new file mode 100644 index 0000000..00876b3 --- /dev/null +++ b/tests/Feature/Commands/ServersSshCommandTest.php @@ -0,0 +1,103 @@ +clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh 1 johndoe') + ->expectsQuestion('Do you want to set "johndoe" as your default SSH user? (y/n)', 'y') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [johndoe]...') + ->assertExitCode(255); +}); + +test('ssh command with server ID supplied and no SSH user', function () { + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh 1') + ->expectsQuestion('Which user would you like to connect as', 'johndoe') + ->expectsQuestion('Do you want to set "johndoe" as your default SSH user? (y/n)', 'y') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [johndoe]...') + ->assertExitCode(255); +}); + +test('ssh command with no server ID supplied', function () { + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers?page=1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ], + ])) + ); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh') + ->expectsQuestion('Which server would you like to start an SSH session for', '1') + ->expectsQuestion('Which user would you like to connect as', 'johndoe') + ->expectsQuestion('Do you want to set "johndoe" as your default SSH user? (y/n)', 'y') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [johndoe]...') + ->assertExitCode(255); +}); + +test('ssh command with server ID supplied and default SSH user', function () { + setTestConfigFile([ + 'ssh_user' => 'janedoe', + ]); + + $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers/1', [])->andReturn( + new Response(200, [], json_encode([ + 'data' => [ + 'id' => 1, + 'name' => 'hellfishmedia', + 'ip_address' => '123.123.123.123', + 'ssh_port' => 22, + ], + ])) + ); + + $this->artisan('servers:ssh 1') + ->expectsOutput('Establishing a secure connection to [hellfishmedia] as [janedoe]...') + ->assertExitCode(255); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 47e79d7..a705613 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -21,7 +21,7 @@ uses(TestCase::class, CreatesApplication::class) ->beforeEach(function () { $this->clientMock = Mockery::mock(Client::class); - $this->spinupwp = resolve(SpinupWp::class)->setClient($this->clientMock)->setApiKey('123'); + $this->spinupwp = resolve(SpinupWp::class)->setClient($this->clientMock)->setApiKey('123'); config()->set('app.ssh_timeout', -1); }) ->in('Feature', 'Unit'); @@ -52,14 +52,14 @@ | */ -function setTestConfigFile() +function setTestConfigFile($profileData = []) { $config = resolve(Configuration::class); file_put_contents($config->configFilePath(), json_encode([ - 'default' => [ + 'default' => array_merge([ 'api_token' => 'myapikey123', 'format' => 'json', - ], + ], $profileData), ], JSON_PRETTY_PRINT)); } From af3668c71e5b75acdab1221fd4b7a17816e4db8d Mon Sep 17 00:00:00 2001 From: A5hleyRich Date: Mon, 22 Nov 2021 17:03:27 +0000 Subject: [PATCH 16/23] Apply php-cs-fixer changes --- tests/Pest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Pest.php b/tests/Pest.php index a705613..f75356d 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -21,7 +21,7 @@ uses(TestCase::class, CreatesApplication::class) ->beforeEach(function () { $this->clientMock = Mockery::mock(Client::class); - $this->spinupwp = resolve(SpinupWp::class)->setClient($this->clientMock)->setApiKey('123'); + $this->spinupwp = resolve(SpinupWp::class)->setClient($this->clientMock)->setApiKey('123'); config()->set('app.ssh_timeout', -1); }) ->in('Feature', 'Unit'); From a49e0f9ab616177278a0347ac519537f9d251981 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Mon, 22 Nov 2021 20:56:46 +0000 Subject: [PATCH 17/23] Clean up configure command --- app/Commands/ConfigureCommand.php | 27 ++++++++++++------- .../Feature/Commands/ConfigureCommandTest.php | 2 +- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/Commands/ConfigureCommand.php b/app/Commands/ConfigureCommand.php index 399d757..baf17eb 100644 --- a/app/Commands/ConfigureCommand.php +++ b/app/Commands/ConfigureCommand.php @@ -19,16 +19,14 @@ public function handle(): int } if (!empty($this->config->get('api_token', $profile))) { - $this->alert("A profile named {$profile} is already configured"); - $response = $this->ask('Do you want to overwrite the existing configuration? (y/n)', 'y'); + $this->alert("A profile named \"{$profile}\" is already configured"); - while (!in_array($response, ['y', 'n'])) { - $this->error("Please type 'y' or 'n'"); - $response = $this->ask('Do you want to overwrite the existing configuration? (y/n)', 'y'); - } + do { + $response = strtolower($this->ask('Do you want to overwrite the existing profile? (y/n)', 'y')); + } while (!in_array($response, ['y', 'n'])); if ($response === 'n') { - return 0; + return self::SUCCESS; } } @@ -41,14 +39,23 @@ public function handle(): int $defaultFormat = null; while (!in_array($defaultFormat, config('app.output_formats'))) { - $defaultFormat = $this->ask('Default output format (json/table)', null); + $defaultFormat = $this->anticipate('Default output format (json/table)', [ + 'json', + 'table', + ], 'table'); } $this->config->set('api_token', $apiKey, $profile); $this->config->set('format', $defaultFormat, $profile); - $this->info('SpinupWP CLI configured successfully'); - return 0; + $this->info('Profile configured successfully.'); + + if ($profile !== 'default') { + $this->line('To use this profile, add the --profile option to your command:'); + $this->line("`spinupwp servers:list --profile={$profile}`"); + } + + return self::SUCCESS; } protected function action(): int diff --git a/tests/Feature/Commands/ConfigureCommandTest.php b/tests/Feature/Commands/ConfigureCommandTest.php index 6f9103d..a6edfbd 100644 --- a/tests/Feature/Commands/ConfigureCommandTest.php +++ b/tests/Feature/Commands/ConfigureCommandTest.php @@ -13,7 +13,7 @@ $this->artisan('configure') ->expectsQuestion('SpinupWP API token', 'my-spinupwp-api-token') ->expectsQuestion('Default output format (json/table)', 'json') - ->expectsOutput('SpinupWP CLI configured successfully'); + ->expectsOutput('Profile configured successfully.'); expect((resolve(Configuration::class))->get('api_token'))->toEqual('my-spinupwp-api-token'); }); From 5283f30a7f198a858e2c8f010c2b826de610263c Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Mon, 29 Nov 2021 13:15:44 +0000 Subject: [PATCH 18/23] Fix merge issues --- app/Commands/BaseCommand.php | 5 +- app/Commands/Concerns/InteractsWithIO.php | 45 ++++++++++++++++ app/Commands/Servers/GetCommand.php | 64 ++++++++++++----------- app/Commands/Servers/ListCommand.php | 5 +- app/Commands/Servers/SshCommand.php | 2 - app/Commands/Sites/GetCommand.php | 21 +++++--- app/Commands/Sites/ListCommand.php | 5 +- app/Commands/Sites/SshCommand.php | 2 - 8 files changed, 98 insertions(+), 51 deletions(-) diff --git a/app/Commands/BaseCommand.php b/app/Commands/BaseCommand.php index 1914507..52fc8b1 100644 --- a/app/Commands/BaseCommand.php +++ b/app/Commands/BaseCommand.php @@ -2,16 +2,17 @@ namespace App\Commands; +use App\Commands\Concerns\InteractsWithIO; use App\Helpers\Configuration; use DeliciousBrains\SpinupWp\SpinupWp; use Exception; use GuzzleHttp\Client; use LaravelZero\Framework\Commands\Command; -use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Symfony\Component\Console\Helper\Table; abstract class BaseCommand extends Command { + use InteractsWithIO; + protected Configuration $config; protected SpinupWp $spinupwp; diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php index 6cdad0c..05f4236 100644 --- a/app/Commands/Concerns/InteractsWithIO.php +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -4,14 +4,24 @@ use Illuminate\Support\Collection; use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Question\ChoiceQuestion; trait InteractsWithIO { protected function format($resource): void { + if (empty($resource) || ($resource instanceof Collection && $resource->isEmpty())) { + return; + } + $this->setStyles(); + if ($this->largeOutput && $this->displayFormat() === 'table') { + $this->largeOutput($resource); + return; + } + if ($this->displayFormat() === 'table') { $this->toTable($resource); return; @@ -119,4 +129,39 @@ public function isAssoc(array $array): bool return (int) $this->output->askQuestion($question); } + + protected function largeOutput(array $resource): void + { + $table = new Table($this->output); + $rows = []; + + foreach ($resource as $key => $value) { + $rows[] = ['' . $key . '', $value]; + } + + $table->setRows($rows)->setStyle('default'); + + if (!empty($this->columnsMaxWidths)) { + foreach ($this->columnsMaxWidths as $column) { + $table->setColumnMaxWidth($column[0], $column[1]); + } + } + + $table->render(); + } + + protected function formatBytes(int $bytes, int $precision = 1, bool $trueSize = false): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + $block = ($trueSize) ? 1024 : 1000; + + $bytes = max($bytes, 0); + $pow = floor(($bytes ? log($bytes) : 0) / log($block)); + $pow = min($pow, count($units) - 1); + $bytes /= pow($block, $pow); + + $total = ($trueSize || $precision > 0) ? round($bytes, $precision) : floor($bytes); + + return $total . ' ' . $units[$pow]; + } } diff --git a/app/Commands/Servers/GetCommand.php b/app/Commands/Servers/GetCommand.php index ba839ff..a512b9c 100644 --- a/app/Commands/Servers/GetCommand.php +++ b/app/Commands/Servers/GetCommand.php @@ -6,46 +6,50 @@ class GetCommand extends BaseCommand { - protected $signature = 'servers:get {server_id} {--format=} {--profile=}'; + protected $signature = 'servers:get + {server_id : The server to output} + {--format=} + {--profile=}'; protected $description = 'Get a server'; protected bool $largeOutput = true; - public function action() + public function action(): int { $this->columnsMaxWidths[] = [1, 50]; $serverId = $this->argument('server_id'); - - $server = $this->spinupwp->servers->get($serverId); - - if ($this->displayFormat() === 'json') { - return $server; + $server = $this->spinupwp->servers->get((int)$serverId); + + if ($this->displayFormat() === 'table') { + $server = [ + 'ID' => $server->id, + 'Name' => $server->name, + 'Provider Name' => $server->provider_name, + 'IP Address' => $server->ip_address, + 'SSH Port' => $server->ssh_port, + 'Ubuntu' => $server->ubuntu_version, + 'Timezone' => $server->timezone, + 'Region' => $server->region, + 'Size' => $server->size, + 'Disk Space' => $this->formatBytes($server->disk_space['used']) . ' of ' . $this->formatBytes($server->disk_space['total'], 0) . ' used', + 'Database Server' => $server->database['server'], + 'Database Host' => $server->database['host'], + 'Database Port' => $server->database['port'], + 'SSH Public Key' => $server->ssh_publickey, + 'Git Public Key' => $server->git_publickey, + 'Connection Status' => ucfirst($server->connection_status), + 'Reboot Required' => $server->reboot_required ? 'Yes' : 'No', + 'Upgrade Required' => $server->upgrade_required ? 'Yes' : 'No', + 'Install Notes' => $server->install_notes ?? '', + 'Created At' => $server->created_at, + 'Status' => ucfirst($server->status), + ]; } - return [ - 'ID' => $server->id, - 'Name' => $server->name, - 'Provider Name' => $server->provider_name, - 'IP Address' => $server->ip_address, - 'SSH Port' => $server->ssh_port, - 'Ubuntu' => $server->ubuntu_version, - 'Timezone' => $server->timezone, - 'Region' => $server->region, - 'Size' => $server->size, - 'Disk Space' => $this->formatBytes($server->disk_space['used']) . ' of ' . $this->formatBytes($server->disk_space['total'], 0) . ' used', - 'Database Server' => $server->database['server'], - 'Database Host' => $server->database['host'], - 'Database Port' => $server->database['port'], - 'SSH Public Key' => $server->ssh_publickey, - 'Git Public Key' => $server->git_publickey, - 'Connection Status' => ucfirst($server->connection_status), - 'Reboot Required' => $server->reboot_required ? 'Yes' : 'No', - 'Upgrade Required' => $server->upgrade_required ? 'Yes' : 'No', - 'Install Notes' => $server->install_notes ?? '', - 'Created At' => $server->created_at, - 'Status' => ucfirst($server->status), - ]; + $this->format($server); + + return self::SUCCESS; } } diff --git a/app/Commands/Servers/ListCommand.php b/app/Commands/Servers/ListCommand.php index 16b261e..cc30771 100644 --- a/app/Commands/Servers/ListCommand.php +++ b/app/Commands/Servers/ListCommand.php @@ -3,12 +3,9 @@ namespace App\Commands\Servers; use App\Commands\BaseCommand; -use App\Commands\Concerns\InteractsWithIO; class ListCommand extends BaseCommand { - use InteractsWithIO; - protected $signature = 'servers:list {--format=} {--profile=}'; @@ -21,7 +18,7 @@ protected function action(): int if ($servers->isEmpty()) { $this->warn('No servers found.'); - return $servers; + return self::SUCCESS; } if ($this->displayFormat() === 'table') { diff --git a/app/Commands/Servers/SshCommand.php b/app/Commands/Servers/SshCommand.php index c93ba73..8526f1d 100644 --- a/app/Commands/Servers/SshCommand.php +++ b/app/Commands/Servers/SshCommand.php @@ -3,12 +3,10 @@ namespace App\Commands\Servers; use App\Commands\BaseCommand; -use App\Commands\Concerns\InteractsWithIO; use App\Commands\Concerns\InteractsWithRemote; class SshCommand extends BaseCommand { - use InteractsWithIO; use InteractsWithRemote; protected $signature = 'servers:ssh diff --git a/app/Commands/Sites/GetCommand.php b/app/Commands/Sites/GetCommand.php index f10e718..e8856e5 100644 --- a/app/Commands/Sites/GetCommand.php +++ b/app/Commands/Sites/GetCommand.php @@ -7,23 +7,28 @@ class GetCommand extends BaseCommand { - protected $signature = 'sites:get {site_id} {--format=} {--profile=}'; + protected $signature = 'sites:get + {site_id : The site to output} + {--format=} + {--profile=}'; protected $description = 'Get a site'; protected bool $largeOutput = true; - public function action() + public function action(): int { - $site = $this->spinupwp->sites->get($this->argument('site_id')); + $site = $this->spinupwp->sites->get((int)$this->argument('site_id')); if ($this->displayFormat() === 'json') { - return $site; + $this->toJson($site); + return self::SUCCESS; } + $additionalDomains = ''; if (!empty($site->additional_domains)) { - $additionalDomains = implode(PHP_EOL, array_map(fn ($domain) => $domain['domain'], $site->additional_domains)); + $additionalDomains = implode(PHP_EOL, array_map(fn($domain) => $domain['domain'], $site->additional_domains)); } $data = [ @@ -59,12 +64,14 @@ public function action() $data['Created At'] = $site->created_at; $data['Status'] = ucfirst($site->status); - return $data; + $this->format($data); + + return self::SUCCESS; } public function backupsData(Site $site, array $data): array { - $scheduledBackups = (bool) $site->backups['next_run_time']; + $scheduledBackups = (bool)$site->backups['next_run_time']; $data['Scheduled Backups'] = $scheduledBackups ? 'Enabled' : 'Disabled'; diff --git a/app/Commands/Sites/ListCommand.php b/app/Commands/Sites/ListCommand.php index ee09943..484a706 100644 --- a/app/Commands/Sites/ListCommand.php +++ b/app/Commands/Sites/ListCommand.php @@ -3,12 +3,9 @@ namespace App\Commands\Sites; use App\Commands\BaseCommand; -use App\Commands\Concerns\InteractsWithIO; class ListCommand extends BaseCommand { - use InteractsWithIO; - protected $signature = 'sites:list {server_id? : Only list sites belonging to this server} {--format=} @@ -28,7 +25,7 @@ protected function action(): int if ($sites->isEmpty()) { $this->warn('No sites found.'); - return $sites; + return self::SUCCESS; } if ($this->displayFormat() === 'table') { diff --git a/app/Commands/Sites/SshCommand.php b/app/Commands/Sites/SshCommand.php index 5adbf41..2e653fa 100644 --- a/app/Commands/Sites/SshCommand.php +++ b/app/Commands/Sites/SshCommand.php @@ -3,12 +3,10 @@ namespace App\Commands\Sites; use App\Commands\BaseCommand; -use App\Commands\Concerns\InteractsWithIO; use App\Commands\Concerns\InteractsWithRemote; class SshCommand extends BaseCommand { - use InteractsWithIO; use InteractsWithRemote; protected $signature = 'sites:ssh From 10492977b0ab3505f59a1a69f3bec8065a435856 Mon Sep 17 00:00:00 2001 From: A5hleyRich Date: Mon, 29 Nov 2021 13:16:10 +0000 Subject: [PATCH 19/23] Apply php-cs-fixer changes --- app/Commands/Servers/GetCommand.php | 2 +- app/Commands/Sites/GetCommand.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Commands/Servers/GetCommand.php b/app/Commands/Servers/GetCommand.php index a512b9c..b68ec05 100644 --- a/app/Commands/Servers/GetCommand.php +++ b/app/Commands/Servers/GetCommand.php @@ -20,7 +20,7 @@ public function action(): int $this->columnsMaxWidths[] = [1, 50]; $serverId = $this->argument('server_id'); - $server = $this->spinupwp->servers->get((int)$serverId); + $server = $this->spinupwp->servers->get((int) $serverId); if ($this->displayFormat() === 'table') { $server = [ diff --git a/app/Commands/Sites/GetCommand.php b/app/Commands/Sites/GetCommand.php index e8856e5..055a850 100644 --- a/app/Commands/Sites/GetCommand.php +++ b/app/Commands/Sites/GetCommand.php @@ -18,7 +18,7 @@ class GetCommand extends BaseCommand public function action(): int { - $site = $this->spinupwp->sites->get((int)$this->argument('site_id')); + $site = $this->spinupwp->sites->get((int) $this->argument('site_id')); if ($this->displayFormat() === 'json') { $this->toJson($site); @@ -28,7 +28,7 @@ public function action(): int $additionalDomains = ''; if (!empty($site->additional_domains)) { - $additionalDomains = implode(PHP_EOL, array_map(fn($domain) => $domain['domain'], $site->additional_domains)); + $additionalDomains = implode(PHP_EOL, array_map(fn ($domain) => $domain['domain'], $site->additional_domains)); } $data = [ @@ -71,7 +71,7 @@ public function action(): int public function backupsData(Site $site, array $data): array { - $scheduledBackups = (bool)$site->backups['next_run_time']; + $scheduledBackups = (bool) $site->backups['next_run_time']; $data['Scheduled Backups'] = $scheduledBackups ? 'Enabled' : 'Disabled'; From 0390e0e8f2120c23c30719c05f20c56264ec75fc Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Mon, 29 Nov 2021 14:26:56 +0000 Subject: [PATCH 20/23] Fix phpstan warnings --- app/Commands/Concerns/InteractsWithIO.php | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php index 05f4236..3b81ff8 100644 --- a/app/Commands/Concerns/InteractsWithIO.php +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -9,6 +9,9 @@ trait InteractsWithIO { + /** + * @param mixed $resource + */ protected function format($resource): void { if (empty($resource) || ($resource instanceof Collection && $resource->isEmpty())) { @@ -53,16 +56,23 @@ protected function displayFormat(): string return $this->option('format'); } - return $this->config->get('format', $this->profile()); + return (string)$this->config->get('format', $this->profile()); } + /** + * @param mixed $resource + */ protected function toJson($resource): void { - $this->line(json_encode($resource->toArray(), JSON_PRETTY_PRINT)); + $this->line((string)json_encode($resource->toArray(), JSON_PRETTY_PRINT)); } + /** + * @param mixed $resource + */ protected function toTable($resource): void { + $rows = []; $tableHeaders = []; if ($resource instanceof Collection) { @@ -74,8 +84,6 @@ protected function toTable($resource): void $tableHeaders = array_keys($firstElement); - $rows = []; - $resource->each(function ($item) use (&$rows) { if (!is_array($item)) { $item->toArray(); @@ -104,7 +112,7 @@ public function askToSelectSite(string $question): int return $this->askToSelect( $question, - $choices->keyBy('id')->map(fn ($site) => $site->domain)->toArray() + $choices->keyBy('id')->map(fn($site) => $site->domain)->toArray() ); } @@ -114,10 +122,13 @@ public function askToSelectServer(string $question): int return $this->askToSelect( $question, - $choices->keyBy('id')->map(fn ($server) => $server->name)->toArray() + $choices->keyBy('id')->map(fn($server) => $server->name)->toArray() ); } + /** + * @param mixed $default + */ protected function askToSelect(string $question, array $choices, $default = null): int { $question = new class($question, $choices, $default) extends ChoiceQuestion { @@ -127,7 +138,7 @@ public function isAssoc(array $array): bool } }; - return (int) $this->output->askQuestion($question); + return (int)$this->output->askQuestion($question); } protected function largeOutput(array $resource): void From 3cd2e5f3789232c16203dc33955676e1b21e778a Mon Sep 17 00:00:00 2001 From: A5hleyRich Date: Mon, 29 Nov 2021 14:27:30 +0000 Subject: [PATCH 21/23] Apply php-cs-fixer changes --- app/Commands/Concerns/InteractsWithIO.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Commands/Concerns/InteractsWithIO.php b/app/Commands/Concerns/InteractsWithIO.php index 3b81ff8..fa86e8b 100644 --- a/app/Commands/Concerns/InteractsWithIO.php +++ b/app/Commands/Concerns/InteractsWithIO.php @@ -56,7 +56,7 @@ protected function displayFormat(): string return $this->option('format'); } - return (string)$this->config->get('format', $this->profile()); + return (string) $this->config->get('format', $this->profile()); } /** @@ -64,7 +64,7 @@ protected function displayFormat(): string */ protected function toJson($resource): void { - $this->line((string)json_encode($resource->toArray(), JSON_PRETTY_PRINT)); + $this->line((string) json_encode($resource->toArray(), JSON_PRETTY_PRINT)); } /** @@ -112,7 +112,7 @@ public function askToSelectSite(string $question): int return $this->askToSelect( $question, - $choices->keyBy('id')->map(fn($site) => $site->domain)->toArray() + $choices->keyBy('id')->map(fn ($site) => $site->domain)->toArray() ); } @@ -122,7 +122,7 @@ public function askToSelectServer(string $question): int return $this->askToSelect( $question, - $choices->keyBy('id')->map(fn($server) => $server->name)->toArray() + $choices->keyBy('id')->map(fn ($server) => $server->name)->toArray() ); } @@ -138,7 +138,7 @@ public function isAssoc(array $array): bool } }; - return (int)$this->output->askQuestion($question); + return (int) $this->output->askQuestion($question); } protected function largeOutput(array $resource): void From e479277bcb5460e29aaf0f0df30ff68ef4bc5d72 Mon Sep 17 00:00:00 2001 From: Ashley Rich Date: Tue, 30 Nov 2021 19:17:35 +0000 Subject: [PATCH 22/23] Fix tests --- tests/Feature/Commands/ServersSshCommandTest.php | 7 ++++++- tests/Feature/Commands/SitesSshCommandTest.php | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Commands/ServersSshCommandTest.php b/tests/Feature/Commands/ServersSshCommandTest.php index 00876b3..e077975 100644 --- a/tests/Feature/Commands/ServersSshCommandTest.php +++ b/tests/Feature/Commands/ServersSshCommandTest.php @@ -51,7 +51,7 @@ test('ssh command with no server ID supplied', function () { $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers?page=1', [])->andReturn( new Response(200, [], json_encode([ - 'data' => [ + 'data' => [ [ 'id' => 1, 'name' => 'hellfishmedia', @@ -59,6 +59,11 @@ 'ssh_port' => 22, ], ], + 'pagination' => [ + 'previous' => null, + 'next' => null, + 'count' => 1, + ], ])) ); diff --git a/tests/Feature/Commands/SitesSshCommandTest.php b/tests/Feature/Commands/SitesSshCommandTest.php index 1c88a1f..0ed939b 100644 --- a/tests/Feature/Commands/SitesSshCommandTest.php +++ b/tests/Feature/Commands/SitesSshCommandTest.php @@ -49,6 +49,11 @@ 'site_user' => 'hellfish', ], ], + 'pagination' => [ + 'previous' => null, + 'next' => null, + 'count' => 1, + ], ])) ); From fe6ce6a8a54248e2be7b2d2c14d50eeb41da2d7d Mon Sep 17 00:00:00 2001 From: A5hleyRich Date: Tue, 30 Nov 2021 19:18:10 +0000 Subject: [PATCH 23/23] Apply php-cs-fixer changes --- tests/Feature/Commands/ServersSshCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Commands/ServersSshCommandTest.php b/tests/Feature/Commands/ServersSshCommandTest.php index e077975..4424464 100644 --- a/tests/Feature/Commands/ServersSshCommandTest.php +++ b/tests/Feature/Commands/ServersSshCommandTest.php @@ -51,7 +51,7 @@ test('ssh command with no server ID supplied', function () { $this->clientMock->shouldReceive('request')->once()->with('GET', 'servers?page=1', [])->andReturn( new Response(200, [], json_encode([ - 'data' => [ + 'data' => [ [ 'id' => 1, 'name' => 'hellfishmedia',