Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions app/Console/Server/ServerAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Bigpixelrocket\DeployerPHP\Contracts\BaseCommand;
use Bigpixelrocket\DeployerPHP\DTOs\ServerDTO;
use Bigpixelrocket\DeployerPHP\Traits\KeyHelpersTrait;
use Bigpixelrocket\DeployerPHP\Traits\KeyValidationTrait;
use Bigpixelrocket\DeployerPHP\Traits\ServerHelpersTrait;
use Bigpixelrocket\DeployerPHP\Traits\ServerValidationTrait;
use Symfony\Component\Console\Attribute\AsCommand;
Expand All @@ -14,14 +16,11 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Add and register a new server to the inventory.
*
* Prompts for server details and saves to inventory.
*/
#[AsCommand(name: 'server:add', description: 'Add a new server to the inventory')]
class ServerAddCommand extends BaseCommand
{
use KeyHelpersTrait;
use KeyValidationTrait;
use ServerHelpersTrait;
use ServerValidationTrait;

Expand All @@ -37,8 +36,8 @@ protected function configure(): void
->addOption('name', null, InputOption::VALUE_REQUIRED, 'Server name')
->addOption('host', null, InputOption::VALUE_REQUIRED, 'Host/IP address')
->addOption('port', null, InputOption::VALUE_REQUIRED, 'SSH port (default: 22)')
->addOption('username', null, InputOption::VALUE_REQUIRED, 'SSH username (default: root)')
->addOption('private-key-path', null, InputOption::VALUE_REQUIRED, 'SSH private key path');
->addOption('private-key-path', null, InputOption::VALUE_REQUIRED, 'SSH private key path')
->addOption('username', null, InputOption::VALUE_REQUIRED, 'SSH username (default: root)');
}

//
Expand All @@ -50,7 +49,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
parent::execute($input, $output);

$this->io->hr();

$this->io->h1('Add New Server');

//
Expand Down Expand Up @@ -120,14 +118,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$privateKeyPathRaw = $this->io->getOptionOrPrompt(
'private-key-path',
fn (): string => $this->io->promptText(
label: 'SSH private key path (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
label: 'Path to SSH private key (leave empty for default ~/.ssh/id_ed25519 or ~/.ssh/id_rsa):',
default: '',
required: false
required: false,
hint: 'Used to connect to the server'
)
);

/** @var ?string $privateKeyPath */
$privateKeyPath = $privateKeyPathRaw !== '' ? $privateKeyPathRaw : null;
$privateKeyPath = $this->resolvePrivateKeyPath($privateKeyPathRaw);

if ($privateKeyPath === null) {
$this->io->error('SSH private key not found.');
$this->io->writeln('');

return Command::FAILURE;
}

//
// Create DTO and display server info
Expand Down
110 changes: 89 additions & 21 deletions app/Console/Server/ServerDeleteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
namespace Bigpixelrocket\DeployerPHP\Console\Server;

use Bigpixelrocket\DeployerPHP\Contracts\BaseCommand;
use Bigpixelrocket\DeployerPHP\Traits\DigitalOceanCommandTrait;
use Bigpixelrocket\DeployerPHP\Traits\ServerHelpersTrait;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Delete a server from the inventory.
*/
#[AsCommand(name: 'server:delete', description: 'Delete a server from the inventory')]
class ServerDeleteCommand extends BaseCommand
{
use DigitalOceanCommandTrait;
use ServerHelpersTrait;

//
Expand All @@ -30,6 +29,7 @@ protected function configure(): void

$this
->addOption('server', null, InputOption::VALUE_REQUIRED, 'Server name')
->addOption('force', null, InputOption::VALUE_NONE, 'Skip typing server name (use with caution)')
->addOption('yes', 'y', InputOption::VALUE_NONE, 'Skip confirmation prompt');
}

Expand All @@ -42,49 +42,91 @@ protected function execute(InputInterface $input, OutputInterface $output): int
parent::execute($input, $output);

$this->io->hr();

$this->io->h1('Delete Server');

//
// Select server

$selection = $this->selectServer();
$server = $this->selectServer();

if ($selection['server'] === null) {
return $selection['exit_code'];
if (is_int($server)) {
return $server;
}

$server = $selection['server'];
$this->displayServerDeets($server);

// Get sites for this server
$serverSites = $this->sites->findByServer($server->name);

if (count($serverSites) > 0) {
$this->io->writeln([' Sites:']);
foreach ($serverSites as $site) {
$this->io->writeln([" • <fg=gray>{$site->domain}</>"]);
}
//
// Display server details

$this->io->writeln('');
$this->io->hr();

$this->displayServerDeets($server, $serverSites);

if (count($serverSites) > 0) {
$this->io->error("Cannot delete server '{$server->name}' because it has one or more sites.");

return Command::FAILURE;
}

//
// Check if DigitalOcean server and initialize API

$isDigitalOceanServer = $this->isDigitalOceanServer($server);

if ($isDigitalOceanServer) {
if ($this->initializeDigitalOceanAPI() === Command::FAILURE) {
$this->io->error('Cannot delete server: DigitalOcean API authentication failed.');
$this->io->writeln([
'',
'You must authenticate with DigitalOcean to delete provisioned servers.',
'The server will not be removed from inventory to prevent orphaned cloud resources.',
'',
]);

return Command::FAILURE;
}
}

//
// Confirm deletion
// Display warning for cloud provider servers

$this->io->writeln('');
if ($isDigitalOceanServer) {
$this->io->writeln('<fg=yellow>⚠ This is a DigitalOcean server.</>');
$this->io->writeln(" Droplet ID: <fg=gray>{$server->dropletId}</>");
$this->io->writeln('');
$this->io->warning('This will:');
$this->io->writeln(' • Destroy the droplet on DigitalOcean');
$this->io->writeln(' • Remove the server from inventory');
$this->io->writeln('');
}

//
// Confirm deletion with extra safety

/** @var bool $forceSkip */
$forceSkip = $input->getOption('force') ?? false;

if (!$forceSkip) {
$typedName = $this->io->promptText(
label: "Type the server name '{$server->name}' to confirm deletion:",
required: true
);

if ($typedName !== $server->name) {
$this->io->error('Server name does not match. Deletion cancelled.');
$this->io->writeln('');

return Command::FAILURE;
}
}

/** @var bool $confirmed */
$confirmed = $this->io->getOptionOrPrompt(
'yes',
fn (): bool => $this->io->promptConfirm(
label: 'Are you sure you want to delete this server?',
default: true
label: 'Are you absolutely sure?',
default: false
)
);

Expand All @@ -96,7 +138,32 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

//
// Delete server
// Destroy cloud provider resources

if ($isDigitalOceanServer && $server->dropletId !== null) {
try {
$this->io->promptSpin(
fn () => $this->digitalOcean->droplet->destroyDroplet($server->dropletId),
"Destroying droplet (ID: {$server->dropletId})"
);
$this->io->success('Droplet destroyed');
} catch (\RuntimeException $e) {
$this->io->error($e->getMessage());
$this->io->writeln('');

$continueAnyway = $this->io->promptConfirm(
label: 'Remove from inventory anyway?',
default: true
);

if (!$continueAnyway) {
return Command::FAILURE;
}
}
}

//
// Delete server from inventory

$this->servers->delete($server->name);

Expand All @@ -109,6 +176,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->io->showCommandHint('server:delete', [
'server' => $server->name,
'yes' => $confirmed,
'force' => true,
]);

return Command::SUCCESS;
Expand Down
33 changes: 7 additions & 26 deletions app/Console/Server/ServerListCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
* List all servers in the inventory.
*/
#[AsCommand(name: 'server:list', description: 'List all servers in the inventory')]
#[AsCommand(name: 'server:list', description: 'List servers in the inventory')]
class ServerListCommand extends BaseCommand
{
use ServerHelpersTrait;
Expand All @@ -30,40 +27,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int
parent::execute($input, $output);

$this->io->hr();
$this->io->h1('List Servers');

//
// Get all servers

$allServers = $this->servers->all();
if (count($allServers) === 0) {
$this->io->warning('No servers found in inventory');
$this->io->writeln([
'',
'Use <fg=cyan>server:add</> to add a server',
'',
]);
$allServers = $this->ensureServersAvailable();

return Command::SUCCESS;
if (is_int($allServers)) {
return $allServers;
}

//
// Display servers with their sites

$this->io->h1('All Servers');

foreach ($allServers as $count => $server) {
$this->displayServerDeets($server);

// Get sites for this server
// Display server with sites
$serverSites = $this->sites->findByServer($server->name);

if (count($serverSites) > 0) {
$this->io->writeln([' Sites:']);
foreach ($serverSites as $site) {
$this->io->writeln([" • <fg=gray>{$site->domain}</>"]);
}
$this->io->writeln('');
}
$this->displayServerDeets($server, $serverSites);

if ($count < count($allServers) - 1) {
$this->io->writeln([
Expand Down
Loading