Skip to content

Commit 5e4bb9f

Browse files
committed
feat: support postgresql database servers
1 parent 19897c2 commit 5e4bb9f

65 files changed

Lines changed: 1809 additions & 333 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"symfony/polyfill-php80": "^1.27",
3939
"symfony/process": "^5.4.46|^6.4.14|^7.0|^8.0",
4040
"symfony/yaml": "^5.4|^6.0|^7.0|^8.0",
41-
"ymirapp/ymir-sdk-php": "^1.3.0"
41+
"ymirapp/ymir-sdk-php": "^1.4.0"
4242
},
4343
"require-dev": {
4444
"fakerphp/faker": "^1.17",

src/ApiClient.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ public function createDatabase(Model\DatabaseServer $databaseServer, string $nam
143143
/**
144144
* Create a new database on the given network.
145145
*/
146-
public function createDatabaseServer(Model\Network $network, string $name, string $type, ?int $storage = 50, bool $public = false): Model\DatabaseServer
146+
public function createDatabaseServer(Model\Network $network, string $engine, string $name, string $type, ?bool $public = null, ?int $storage = null): Model\DatabaseServer
147147
{
148-
return Model\DatabaseServer::fromArray($this->client->createDatabaseServer($network->getId(), $name, $type, $storage, $public)->all());
148+
return Model\DatabaseServer::fromArray($this->client->createDatabaseServer($network->getId(), $engine, $name, $type, $public, $storage)->all());
149149
}
150150

151151
/**

src/Command/Database/AbstractDatabaseTunnelCommand.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,16 @@ protected function getConnection(string $databaseQuestion, string $databaseServe
6969
/**
7070
* Open a SSH tunnel to a private database server.
7171
*/
72-
protected function openSshTunnel(DatabaseServer $databaseServer, int $localPort = 3305): Process
72+
protected function openSshTunnel(DatabaseServer $databaseServer, ?int $localPort = null): Process
7373
{
74+
$localPort = $localPort ?? $databaseServer->getDefaultLocalPort();
75+
7476
if ('available' !== $databaseServer->getStatus() || empty($databaseServer->getEndpoint())) {
7577
throw new InvalidInputException(sprintf('The "%s" database server isn\'t available', $databaseServer->getName()));
7678
} elseif ($databaseServer->isPublic()) {
7779
throw new InvalidInputException(sprintf('The "%s" database server is publicly accessible and isn\'t on a private subnet', $databaseServer->getName()));
78-
} elseif (3306 === $localPort) {
79-
throw new InvalidInputException('Cannot use port 3306 as the local port for the SSH tunnel to the database server');
80+
} elseif ($databaseServer->getDefaultPort() === $localPort) {
81+
throw new InvalidInputException(sprintf('Cannot use port %s as the local port for the SSH tunnel to the database server', $databaseServer->getDefaultPort()));
8082
}
8183

8284
$this->output->info(sprintf('Opening SSH tunnel to the "<comment>%s</comment>" database server...', $databaseServer->getName()));
@@ -87,6 +89,6 @@ protected function openSshTunnel(DatabaseServer $databaseServer, int $localPort
8789
throw new ResourceStateException(sprintf('The "%s" network doesn\'t have a bastion host to connect to, but you can add one to the network with the "%s" command', $databaseServer->getNetwork()->getName(), AddBastionHostCommand::NAME));
8890
}
8991

90-
return $this->sshExecutable->openTunnelToBastionHost($bastionHost, $localPort, $databaseServer->getEndpoint(), 3306);
92+
return $this->sshExecutable->openTunnelToBastionHost($bastionHost, $localPort, $databaseServer->getEndpoint(), $databaseServer->getDefaultPort());
9193
}
9294
}

src/Command/Database/CreateDatabaseServerCommand.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@ protected function configure()
3838
->setName(self::NAME)
3939
->setDescription('Create a new database server')
4040
->addArgument('name', InputArgument::OPTIONAL, 'The name of the database server')
41+
->addOption('engine', null, InputOption::VALUE_REQUIRED, 'The database to create (mysql or postgresql)')
4142
->addOption('network', null, InputOption::VALUE_REQUIRED, 'The ID or name of the network on which the database will be created')
4243
->addOption('private', null, InputOption::VALUE_NONE, 'The created database server won\'t be publicly accessible')
4344
->addOption('public', null, InputOption::VALUE_NONE, 'The created database server will be publicly accessible')
44-
->addOption('serverless', null, InputOption::VALUE_NONE, 'Create an Aurora serverless database cluster (overrides all other options)')
45+
->addOption('serverless', null, InputOption::VALUE_NONE, 'Create an Aurora serverless database cluster for the selected engine')
4546
->addOption('storage', null, InputOption::VALUE_REQUIRED, 'The maximum amount of storage (in GB) allocated to the database server')
4647
->addOption('type', null, InputOption::VALUE_REQUIRED, 'The database server type to create on the cloud provider');
4748
}
@@ -61,8 +62,8 @@ protected function perform()
6162
$this->output->newLine();
6263

6364
$this->output->horizontalTable(
64-
['Database Sever', new TableSeparator(), 'Username', 'Password', new TableSeparator(), 'Type', 'Public', 'Storage (in GB)'],
65-
[[$databaseServer->getName(), new TableSeparator(), $databaseServer->getUsername(), $databaseServer->getPassword(), new TableSeparator(), $databaseServer->getType(), $this->output->formatBoolean($databaseServer->isPublic()), $databaseServer->getStorage() ?? 'N/A']]
65+
['Database Sever', new TableSeparator(), 'Username', 'Password', new TableSeparator(), 'Database', 'Type', 'Public', 'Storage (in GB)'],
66+
[[$databaseServer->getName(), new TableSeparator(), $databaseServer->getUsername(), $databaseServer->getPassword(), new TableSeparator(), $databaseServer->getEngineLabel(), $databaseServer->getType(), $this->output->formatBoolean($databaseServer->isPublic()), $databaseServer->getStorage() ?? 'N/A']]
6667
);
6768

6869
$this->output->infoWithDelayWarning('Database server created');

src/Command/Database/CreateDatabaseUserCommand.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Console\Input\InputOption;
1818
use Ymir\Cli\Command\AbstractCommand;
1919
use Ymir\Cli\Exception\Resource\ProvisioningFailedException;
20+
use Ymir\Cli\Exception\UnsupportedDatabaseServerEngineException;
2021
use Ymir\Cli\Resource\Model\DatabaseServer;
2122
use Ymir\Cli\Resource\Model\DatabaseUser;
2223

@@ -54,6 +55,8 @@ protected function perform()
5455
throw new ProvisioningFailedException('Failed to provision database user');
5556
}
5657

58+
$databaseServer = $databaseUser->getDatabaseServer();
59+
5760
$this->output->horizontalTable(
5861
['Username', 'Password'],
5962
[[$databaseUser->getName(), $databaseUser->getPassword()]]
@@ -63,11 +66,27 @@ protected function perform()
6366
$this->output->newLine();
6467
$this->output->info('Database user created successfully');
6568

66-
if (!$databaseUser->getDatabaseServer()->isPublic()) {
67-
$this->output->newLine();
68-
$this->output->important(sprintf('The "<comment>%s</comment>" database user needs to be manually created on the "<comment>%s</comment>" database server because it isn\'t publicly accessible. You can use the following queries to create it and grant it access to the server:', $databaseUser->getName(), $databaseUser->getDatabaseServer()->getName()));
69-
$this->output->writeln(sprintf('CREATE USER %s@\'%%\' IDENTIFIED BY \'%s\'', $databaseUser->getName(), $databaseUser->getPassword()));
70-
$this->output->writeln(sprintf('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES ON *.* TO %s@\'%%\'', $databaseUser->getName()));
69+
if ($databaseServer->isPublic()) {
70+
return;
71+
}
72+
73+
$this->output->newLine();
74+
$this->output->important(sprintf('The "<comment>%s</comment>" database user needs to be manually created on the "<comment>%s</comment>" database server because it isn\'t publicly accessible. You can use the following queries to create it and grant it access to the server:', $databaseUser->getName(), $databaseServer->getName()));
75+
76+
switch ($databaseServer->getEngine()) {
77+
case DatabaseServer::ENGINE_MYSQL:
78+
$this->output->writeln(sprintf('CREATE USER %s@\'%%\' IDENTIFIED BY \'%s\'', $databaseUser->getName(), $databaseUser->getPassword()));
79+
$this->output->writeln(sprintf('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES ON *.* TO %s@\'%%\'', $databaseUser->getName()));
80+
81+
break;
82+
case DatabaseServer::ENGINE_POSTGRESQL:
83+
$this->output->writeln(sprintf('CREATE USER "%s" WITH PASSWORD \'%s\'', $databaseUser->getName(), $databaseUser->getPassword()));
84+
$this->output->writeln(sprintf('GRANT "%s" TO CURRENT_USER', $databaseUser->getName()));
85+
$this->output->writeln(sprintf('GRANT ALL PRIVILEGES ON DATABASE "<database>" TO "%s"', $databaseUser->getName()));
86+
87+
break;
88+
default:
89+
throw new UnsupportedDatabaseServerEngineException($databaseServer->getEngine());
7190
}
7291
}
7392
}

src/Command/Database/DatabaseServerTunnelCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ protected function configure()
3636
->setName(self::NAME)
3737
->setDescription('Create a SSH tunnel to a database server')
3838
->addArgument('server', InputArgument::OPTIONAL, 'The ID or name of the database server to create a SSH tunnel to')
39-
->addOption('port', null, InputOption::VALUE_REQUIRED, 'The local port to use to connect to the database server', '3305');
39+
->addOption('port', null, InputOption::VALUE_REQUIRED, 'The local port to use to connect to the database server');
4040
}
4141

4242
/**
@@ -45,7 +45,7 @@ protected function configure()
4545
protected function perform()
4646
{
4747
$databaseServer = $this->resolve(DatabaseServer::class, 'Which database server would you like to connect to?');
48-
$localPort = (int) $this->input->getNumericOption('port');
48+
$localPort = $this->input->getNumericOption('port') ?? $databaseServer->getDefaultLocalPort();
4949

5050
if (empty($localPort)) {
5151
throw new InvalidInputException('You must provide a valid "port" option');

src/Command/Database/DeleteDatabaseUserCommand.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Console\Input\InputArgument;
1717
use Symfony\Component\Console\Input\InputOption;
1818
use Ymir\Cli\Command\AbstractCommand;
19+
use Ymir\Cli\Exception\UnsupportedDatabaseServerEngineException;
1920
use Ymir\Cli\Resource\Model\DatabaseServer;
2021
use Ymir\Cli\Resource\Model\DatabaseUser;
2122

@@ -56,10 +57,24 @@ protected function perform()
5657

5758
$this->output->info('Database user deleted');
5859

59-
if (!$databaseServer->isPublic()) {
60-
$this->output->newLine();
61-
$this->output->important('The database user needs to be manually deleted on the database server because it isn\'t publicly accessible. You can use the following query to delete it:');
62-
$this->output->writeln(sprintf('DROP USER IF EXISTS %s@\'%%\'', $databaseUser->getName()));
60+
if ($databaseServer->isPublic()) {
61+
return;
62+
}
63+
64+
$this->output->newLine();
65+
$this->output->important('The database user needs to be manually deleted on the database server because it isn\'t publicly accessible. You can use the following query to delete it:');
66+
67+
switch ($databaseServer->getEngine()) {
68+
case DatabaseServer::ENGINE_MYSQL:
69+
$this->output->writeln(sprintf('DROP USER IF EXISTS %s@\'%%\'', $databaseUser->getName()));
70+
71+
break;
72+
case DatabaseServer::ENGINE_POSTGRESQL:
73+
$this->output->writeln(sprintf('DROP USER IF EXISTS "%s"', $databaseUser->getName()));
74+
75+
break;
76+
default:
77+
throw new UnsupportedDatabaseServerEngineException($databaseServer->getEngine());
6378
}
6479
}
6580
}

src/Command/Database/ExportDatabaseCommand.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@
1818
use Symfony\Component\Console\Input\InputOption;
1919
use Symfony\Component\Filesystem\Filesystem;
2020
use Ymir\Cli\ApiClient;
21+
use Ymir\Cli\Database\Connection;
2122
use Ymir\Cli\Database\Mysqldump;
23+
use Ymir\Cli\Exception\Executable\ExecutableNotDetectedException;
2224
use Ymir\Cli\Exception\InvalidInputException;
2325
use Ymir\Cli\Exception\SystemException;
26+
use Ymir\Cli\Exception\UnsupportedDatabaseServerEngineException;
27+
use Ymir\Cli\Executable\PgDumpExecutable;
2428
use Ymir\Cli\Executable\SshExecutable;
2529
use Ymir\Cli\ExecutionContextFactory;
2630
use Ymir\Cli\Process\Process;
31+
use Ymir\Cli\Resource\Model\DatabaseServer;
2732

2833
class ExportDatabaseCommand extends AbstractDatabaseTunnelCommand
2934
{
@@ -41,14 +46,22 @@ class ExportDatabaseCommand extends AbstractDatabaseTunnelCommand
4146
*/
4247
private $filesystem;
4348

49+
/**
50+
* The pg_dump executable.
51+
*
52+
* @var PgDumpExecutable
53+
*/
54+
private $pgDumpExecutable;
55+
4456
/**
4557
* Constructor.
4658
*/
47-
public function __construct(ApiClient $apiClient, ExecutionContextFactory $contextFactory, Filesystem $filesystem, SshExecutable $sshExecutable)
59+
public function __construct(ApiClient $apiClient, ExecutionContextFactory $contextFactory, Filesystem $filesystem, PgDumpExecutable $pgDumpExecutable, SshExecutable $sshExecutable)
4860
{
4961
parent::__construct($apiClient, $contextFactory, $sshExecutable);
5062

5163
$this->filesystem = $filesystem;
64+
$this->pgDumpExecutable = $pgDumpExecutable;
5265
}
5366

5467
/**
@@ -93,7 +106,9 @@ protected function perform()
93106
$this->output->infoWithDelayWarning(sprintf('Exporting "<comment>%s</comment>" database', $connection->getDatabase()));
94107

95108
try {
96-
Mysqldump::fromConnection($connection, ['compress' => $compression])->start($filename);
109+
$this->exportDatabase($connection, $filename, $compression);
110+
} catch (ExecutableNotDetectedException|UnsupportedDatabaseServerEngineException $exception) {
111+
throw $exception;
97112
} catch (\Throwable $exception) {
98113
throw new SystemException(sprintf('Failed to export database: %s', $exception->getMessage()));
99114
} finally {
@@ -104,4 +119,25 @@ protected function perform()
104119

105120
$this->output->infoWithValue('Database exported successfully to', $filename);
106121
}
122+
123+
/**
124+
* Export the database for the given connection.
125+
*/
126+
private function exportDatabase(Connection $connection, string $filename, string $compression): void
127+
{
128+
$engine = $connection->getDatabaseServer()->getEngine();
129+
130+
switch ($engine) {
131+
case DatabaseServer::ENGINE_MYSQL:
132+
Mysqldump::fromConnection($connection, ['compress' => $compression])->start($filename);
133+
134+
break;
135+
case DatabaseServer::ENGINE_POSTGRESQL:
136+
$this->pgDumpExecutable->dump($connection, $filename, $compression);
137+
138+
break;
139+
default:
140+
throw new UnsupportedDatabaseServerEngineException($engine);
141+
}
142+
}
107143
}

src/Command/Database/GetDatabaseServerInfoCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ protected function perform()
4646
$databaseServer = $this->resolve(DatabaseServer::class, 'Which database server would you like to get information about?');
4747

4848
$this->output->horizontalTable(
49-
['Id', 'Name', 'Status', 'Locked', 'Public', new TableSeparator(), 'Provider', 'Network', 'Region', 'Type', 'Storage', 'Endpoint'],
49+
['Id', 'Name', 'Status', 'Locked', 'Public', new TableSeparator(), 'Provider', 'Network', 'Region', 'Database', 'Type', 'Storage', 'Endpoint'],
5050
[[
5151
$databaseServer->getId(),
5252
$databaseServer->getName(),
@@ -57,6 +57,7 @@ protected function perform()
5757
$databaseServer->getNetwork()->getProvider()->getName(),
5858
$databaseServer->getNetwork()->getName(),
5959
$databaseServer->getRegion(),
60+
$databaseServer->getEngineLabel(),
6061
$databaseServer->getType(),
6162
$databaseServer->getStorage() ? $databaseServer->getStorage().'GB' : 'N/A',
6263
$databaseServer->getEndpoint() ?? 'pending',

0 commit comments

Comments
 (0)